01 · An interactive explainer

Loop Engineering: Designing the System, Not the Prompt

From prompting an agent once, to designing a system that keeps improving the app until the current state matches the desired state.

Throughout, we use one concrete stack — Claude Code, Vite, and GitHub Actions — to make the loop real. But the loop is the idea, not the tools. The same five moves — written target, honest measurement, feedback, correction, stopping condition — apply to a data pipeline, a content workflow, or any process where you can describe "done" and check for it.

02 · The shift

Normal AI coding vs. loop engineering

Both approaches use an AI agent to write code. The difference is where the target, the measurement, and the memory live.

Normal AI coding

Normal AI-assisted coding is conversational. You ask the AI to build something, review the result, then ask again. This can work — but the target state, measurement rules, and correction logic often live only in your head.

  • No explicit target state
  • No formal measurement
  • No task memory
  • No automated test signal
  • No clear stopping condition

Loop engineering

Loop engineering makes the target, measurement, feedback, and correction cycle explicit. The AI is no longer just responding to prompts. It is working inside a system.

  • End-state document
  • Test plan
  • Task log
  • GitHub Actions
  • Failure feedback
  • Corrective prompts
  • Acceptance criteria

03 · The core idea

The two states

Every loop is powered by one comparison: what exists now, versus what should exist eventually. Click each box to see where that state lives.

gap

Current state — this is where it lives:

  • Your project folder
  • Your source code
  • Your tests
  • Your GitHub repository
  • Your latest deployment
  • Your task log
  • Your latest GitHub Actions result

For a Vite app, the current state includes files such as:

src/App.tsx
src/lib/calculateSalesTax.ts
tests/calculateSalesTax.test.ts
package.json
.github/workflows/test.yml
TASKS.md

Desired end state — the written description of what the app should eventually become.

It should live in your project folder, usually as:

END_STATE.md
PRODUCT_SPEC.md
ACCEPTANCE_CRITERIA.md
DESIGN_SPEC.md
TEST_PLAN.md

It defines:

  • What the app does
  • Who it is for
  • What features must exist
  • What success means
  • What tests must pass
  • What counts as done

The gap

The gap is the difference between what exists now and what should exist eventually. Loop engineering is the repeated process of reducing this gap.

04 · The workspace

Where does everything live?

One project folder holds the destination, the rules, the memory, the code, and the measurement. Click any file to see its job.

sales-tax-calculator/
App.tsx
lib/
calculateSalesTax.ts
calculateSalesTax.test.ts
.github/
workflows/
Click a file in the tree ←

Each file plays one role in the loop. The panel will explain it here.

The documents define the target.
The code defines the current app.
The tests measure reality.
GitHub Actions reports the signal.
Claude Code performs the correction.

05 · The engine

The feedback loop

This is the cycle the whole system exists to run. Click any step to see what happens, where, who does it, and what artifact is involved.

The loop is not a file. The loop is the cycle you design around the files.
06 · Make it concrete

The sales tax calculator example

A small fictional project. The app should accept item price, quantity, tax rate, and an optional discount — then calculate subtotal, discount, tax, and final total, validate bad input, and cover every requirement with automated tests. Here is what each loop artifact looks like.

This is the destination. Everything else in the loop exists to move the project toward this document.

# End State: Sales Tax Calculator
The app allows a user to enter item price, quantity, tax rate,
and optional discount.

It calculates:
- Subtotal
- Discount amount
- Taxable amount
- Sales tax
- Final total

Acceptance criteria:
- All currency values rounded to 2 decimal places
- Invalid input shows clear error messages
- Calculation logic is separated from UI
- Every requirement above is covered by automated tests
- GitHub Actions runs tests on every push

Writing your own: acceptance criteria come in two kinds. Machine-checkable — a test can verify it (rounding, validation, calculated totals). Judgment-checkable — someone must review it against a written description ("calculation logic is separated from UI"). Write both, and know which is which: tests measure the first kind; the final end-state review covers the second. If a criterion is neither testable nor judgeable from what you wrote, it will cause thrash — rewrite it.

The operating rules that keep the loop honest. Rule 4 matters most — it is what stops the agent from making tests pass by weakening them.

# Loop rules
Each loop iteration:
1. Read END_STATE.md, TASKS.md, and TEST_PLAN.md before touching code
2. Make one small improvement per loop — the smallest useful step
3. Run tests after every change
4. Never modify or delete a test to make it pass
   If a test looks wrong, stop and ask — do not "fix" the test
5. Update TASKS.md before finishing:
   what changed, what failed, what is next
6. Commit only when tests pass locally
7. If the same test fails 3 loops in a row, stop and report
8. Never mark a criterion done without evidence:
   a passing test, or an explicit review

The human-readable measurement strategy. Tests turn "does it work?" into a yes/no signal. And cases are cheap: for a calculator, one table of inputs and expected outputs generates dozens of tests — coverage of the requirements is the goal, not the count.

# Test Plan
Test:
- Standard tax calculation
- Quantity greater than 1
- Zero tax
- Decimal tax rate
- Discount before tax
- Rounding edge cases
- Negative price
- Invalid quantity
- Tax rate above 100
- Discount above 100

Success condition:
All tests pass locally and in GitHub Actions.

Who writes the tests — and how do you know they're the right ones? You describe what matters in plain language (this test plan is exactly that); the agent turns each line into a test; the final end-state review catches whatever the tests missed. You never have to write test code to direct the loop — but someone does have to judge whether the tests are honest.

For the engineers: a passing test only proves the code does what the test says — not what the requirement needs. Watch for tests that can't fail (asserting a value against itself, or re-deriving the expected result with the same code being tested), tests pinned to whatever the code happened to return rather than to the spec, and "coverage" that runs a line without checking its output. A good test states the expected answer independently — expect(tax(100, 0.08)).toBe(8.00), a number you worked out by hand — so that when the code is wrong, the test goes red. "The agent wrote a test" is not the same as "the requirement is covered." Reviewing tests against END_STATE.md is a real job, and on the loop it is mostly yours.

The project's memory. Without it, every loop starts from zero.

# Task Log

## Backlog
- [ ] Create calculation function
- [ ] Add unit tests
- [ ] Build input form
- [ ] Add validation
- [ ] Add GitHub Actions
- [ ] Final review against END_STATE.md

## Completed
- [x] Project initialized
- [x] END_STATE.md created
- [x] LOOP.md created

## Failed Tests / Issues
None yet.

This tells GitHub: whenever new code is pushed, install the project and run the tests. It re-runs tests Claude already ran because a clean machine catches problems that only appear outside your setup — and the result is recorded next to every commit, in the repository’s Actions tab.

name: Run Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v4
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Install dependencies
        run: npm install
      - name: Run tests
        run: npm test

One prompt runs one loop iteration. Notice it points at documents, not at your memory of the conversation.

Read END_STATE.md, LOOP.md, TASKS.md, and TEST_PLAN.md.
Inspect the current codebase.
Identify the biggest gap between the current state
and the desired end state.
Choose the smallest useful improvement.
Implement it.
Run tests.
If tests fail, diagnose and fix them.
Update TASKS.md.
Tell me what changed and what the next loop should do.

A fictional failure, straight from the test runner:

FAILED: should round final total to 2 decimal places
Expected: 21.65
Received: 21.64875

This is not just an error. It is a feedback signal. The signal says:

  • The current state does not satisfy the rounding requirement
  • The calculation logic needs correction
  • The test should remain as a guardrail
  • Claude should fix the smallest relevant part of the code

A failure doesn't break the loop — it drives it. The cycle below runs until the signal turns green.

Failure detected Map failure to END_STATE.md Identify violated requirement Fix calculation logic Rerun tests Update TASKS.md Commit and push GitHub Actions tests again
07 · The honest part

When loops go wrong

A loop is not magic — it fails in predictable ways. Four failure modes account for most stuck projects. Each has a counter-rule, and the counter-rules are exactly what LOOP.md exists to hold.

The agent weakens a test to make it pass

The most common failure. Instead of fixing the code, the agent edits the expected value, loosens the assertion, or deletes the test. Everything turns green; nothing is fixed.

The ruleTests are guardrails. Never modify a test to make it pass — change the code, or stop and ask. Write this into LOOP.md.

Tests pass, but the app is still wrong

Tests only measure what someone thought to write down. A green check means "the written checks pass" — not "the app is correct."

The ruleReview against END_STATE.md, not against the test count. When you find a miss, capture it as a new test so it stays caught.

An ambiguous end state causes thrash

If END_STATE.md says "clean UI" or "handles errors well," every loop interprets it differently — and the agent rewrites the same code back and forth.

The ruleEvery criterion must be testable by a machine or judgeable from a written description. If you can't say how it would be checked, rewrite it.

The loop churns without stopping

Without a stopping condition, the agent keeps "improving" forever — refactoring, polishing, and drifting away from the target.

The ruleDone is defined by the acceptance criteria, nowhere else. Add a circuit breaker: if the same test fails three loops in a row, stop and report.

08 · The cast

Who does what?

Four participants, four distinct jobs. The loop works because none of them tries to do another's job.

You

  • Define the desired end state
  • Decide what matters
  • Review major decisions
  • Approve direction
  • Clarify ambiguous product choices

Claude Code

  • Read project documents
  • Inspect codebase
  • Write code
  • Run local tests
  • Diagnose failures
  • Update task logs
  • Commit and push when instructed

GitHub

  • Store the code
  • Track commits
  • Preserve history
  • Show what changed

GitHub Actions

  • Detect new code pushes
  • Run test instructions
  • Produce pass/fail signal
  • Store failure logs
Claude Code builds. GitHub Actions measures. The end-state document judges. The task log remembers.
09 · Setup

The practical setup checklist

Everything you need before the first loop runs. Tick items as you go — your progress is saved in this browser.

0 of 14 done

  • One folder is the boundary of the loop. Everything the loop reads or writes lives inside it.
  • Without a written destination, "done" is a feeling, not a fact. This file makes it checkable.
  • Operating rules for the agent: how big each improvement should be, when to test, when to stop and ask.
  • The agent has no memory between sessions. This file carries what's done, what failed, and what's next.
  • Defines what must be checked before tests exist in code. It seeds the automated test suite.
  • Tests can't measure "looks right." This document gives the visual and UX target a written form.
  • Creates the actual current state — a running codebase the loop can inspect and improve. Prompt 0 in the starter pack has Claude Code do this for you, test runner included.
  • Even one test turns "run the loop" into something measurable. The suite grows from here.
  • Moves measurement out of your hands. Every push gets tested automatically, whether you remember or not.
  • The first iteration proves the system works end to end: read docs → find gap → improve → test → log.
  • Pushing is what triggers the external signal. Uncommitted work is invisible to the loop.
  • The green check or red cross is the loop's verdict on the latest current state. Find it in the repository's Actions tab, or as the ✓ / ✕ next to the commit.
  • A failure log pasted into the agent is the correction signal. This is the loop closing.
  • The stopping condition lives in END_STATE.md — not in your patience level.
10 · Try it

Run a simulated loop

Each click advances one build cycle of the sales tax calculator. Watch the current state close the gap toward the desired end state.

Ready — click "Run loop step" to start

Current stateEmpty Vite project
GapEverything
Action
Test result
11 · Take it with you

Copy-paste starter pack

Five prompts that cover the whole cycle: scaffold, initialize, correct, review, refine. Copy them straight into Claude Code.

Prompt 0 · Scaffold the project

You are working inside an empty project folder.
Set up a loop-engineering project for: [describe your app in one sentence].
1. Initialize a Vite app
2. Install a test runner (Vitest) and wire it to "npm test"
3. Create END_STATE.md — draft acceptance criteria from my description, and mark anything ambiguous with a question for me
4. Create LOOP.md with the loop rules, plus TASKS.md, TEST_PLAN.md, and DESIGN_SPEC.md
5. Add one passing test to prove "npm test" works
6. Add .github/workflows/test.yml so tests run on every push
Stop after setup. Show me END_STATE.md for review before running the first loop.

Prompt 1 · Initialize loop

You are working inside this project folder.
This project must be built using loop engineering.
First, read:
- END_STATE.md
- LOOP.md
- TASKS.md
- TEST_PLAN.md
- DESIGN_SPEC.md, if present
Treat END_STATE.md as the desired final state.
Treat LOOP.md as your operating instructions.
Treat TASKS.md as the project memory.
Inspect the current codebase, identify the biggest gap, choose the smallest useful improvement, implement it, run tests, update TASKS.md, and report what changed.

Prompt 2 · Fix failed GitHub Actions

GitHub Actions failed.
Treat this as feedback from the loop.
Please:
1. Identify which test failed
2. Explain what requirement it relates to
3. Explain why the current code failed
4. Fix the smallest necessary part of the code
5. Run tests locally
6. Update TASKS.md
7. Commit and push the fix

Prompt 3 · Final end-state review

Compare the entire current codebase against END_STATE.md.
Create a gap analysis with:
- Completed requirements
- Partially completed requirements
- Missing requirements
- Broken or risky areas
- Recommended next loop actions
Do not write new code until the gap analysis is complete.

Prompt 4 · Design review

Review the current UI against DESIGN_SPEC.md.
Identify:
- What matches the design intent
- What feels unclear
- What should be simplified
- What should be more visually prominent
- What should be improved for mobile users
Suggest the smallest useful design improvements.