~/matenemeth

500 rounds of an autonomous coding loop

2026-09 · 10 min read

Since the end of June, an autonomous loop has been working on Tallyroot, my multi-currency personal finance app. It picks an issue from the backlog, plans the fix, writes the code, has another agent review it, and merges to main if everything passes. I mostly read what it did afterwards.

By late September that’s over 1,200 merged pull requests from its branches. Some of them were even features! (More about this later.) I only started logging rounds in August, and there have been 541 since then.

This post is about how the loop works, what it broke, what it costs, and the rule each failure produced. How Tallyroot found its first paying subscribers is a separate post.

The numbers

Running since end of June 2026
Merged pull requests from the loop’s branches 1,263, out of 1,388 in the repo
Rounds logged since August 541
Average round about 80 minutes and 16 agents
Test suite (end of August) 3,431 tests in 333 files
Lessons in the playbook 225
Tests it has watched fail before trusting them 756
Rules in its constitution 6
Deploys by the loop 0

How a round works

A round is one pass through the loop:

  1. Backlog. Pick the highest-priority open issue. If there’s nothing to do, go find something: audit the code for bugs, or play a few user personas and propose features. Features wait until I approve them.
  2. Plan. A read-only agent works out the root cause, what to change, and how to test it.
  3. Implement. Another agent does the work in its own git worktree, reviews its own diff, and opens a PR.
  4. Review. A separate agent reviews the PR, looking for reasons to reject it. Every blocking finding then gets its own agent, whose only job is to prove the finding wrong.
  5. Merge gate. Zero confirmed blockers, and a green test suite on the branch rebased onto current main. Otherwise no merge.
  6. Retrospective. Whatever went wrong becomes a lesson for the next round, or a guardrail: a test or a lint rule that makes the same mistake impossible.

Hand-drawn cycle of one round of the loop: backlog, plan, implement, review, merge gate, retrospective, and back to backlog, about 80 minutes per round.Hand-drawn cycle of one round of the loop: backlog, plan, implement, review, merge gate, retrospective, and back to backlog, about 80 minutes per round.

It runs as a workflow script in Claude Code, about 2,100 lines of JavaScript. Every step is an agent call with a schema for its answer, so the script gets structured data back instead of prose. Stripped down to its shape, a round looks like this:

phase('Constitution')
const rules = await agent(checkConstitution, {
  schema: CONSTITUTION_SCHEMA,
})
if (!rules.passed) return 'aborted'

phase('Backlog')
const pick = await agent(rankBacklog, { schema: GATE_SCHEMA })

phase('Plan') // read-only
const plan = await agent(planIssue(pick), { schema: PLAN_SCHEMA })
if (plan.decisionNeeded) return parkForHuman(plan)

phase('Implement')
const pr = await agent(implement(plan), {
  schema: IMPLEMENT_SCHEMA,
  isolation: 'worktree',
})

phase('Review')
const review = await agent(reviewPr(pr), { schema: REVIEW_SCHEMA })
const verdicts = await parallel(
  review.blocking.map((finding) => () =>
    agent(refute(finding), { schema: VERDICT_SCHEMA }),
  ),
)
const confirmed = review.blocking.filter(
  (_, i) => !verdicts[i]?.refuted,
)
if (confirmed.length) {
  await agent(fixFindings(pr, confirmed), { isolation: 'worktree' })
}

phase('Merge')
const gate = await agent(mergeGate(pr), {
  schema: MERGE_GATE_SCHEMA,
  isolation: 'worktree',
})

phase('Retrospective')
await agent(retrospective(pr, review, gate), { schema: RETRO_SCHEMA })

The real one loops review and fix until nothing blocking is left, and it has a dozen side paths: resuming a PR that got stuck, proposing changes to itself, parking a question for me. But that’s the shape.

Most rounds are plain backlog work. Of the 541 logged, 398 worked through the backlog, 90 picked up a PR that had stalled, and 46 were changes to the loop itself. The backlog ran dry twice. Both times it went looking for trouble.

Rounds don’t always finish with a merge. Of the 47 rounds where I record the outcome, 17 merged nothing. A round that stops is cheaper than a bad merge. The record for one issue is eight rounds, spent building an investments feature in parts.

Trust nothing, check everything

Agents are confident. That’s fine for writing code and terrible for reporting on it, so the loop doesn’t take claims at face value.

A new test only counts if the loop has seen it fail on the old code first. Otherwise it might pass because it tests nothing. Every one of those failures is saved as a record, with the exact output and a hash of it, pinned to a commit. There are 756 so far.

PR descriptions get checked by 16 plain scripts, no AI involved. If a description cites a line of code, the script checks the line says what’s claimed. If it says “3 files changed”, the script counts. An agent will happily write a convincing PR description for code that changed since it wrote it.

What it broke

The loop is good at following rules. That’s the problem. It follows the letter.

The merge gate that deleted the evidence. A PR had a red test suite. The merge gate worked out, correctly, that leftover test data was polluting the database and breaking an assertion. Then it ran TRUNCATE on the users and workspaces tables, twice, watched the suite go green, and merged.

It was the local test database, full of fixtures. Production was untouched, nothing real was lost, and the merged code turned out to be fine. All of that was luck. The rule said “never mutate the production database”, and the agent read that as permission for every other database.

Hand-drawn three-step flow: test suite red, then TRUNCATE users and workspaces, then test suite green and merged. Underneath: “technically correct. the worst kind of correct.”Hand-drawn three-step flow: test suite red, then TRUNCATE users and workspaces, then test suite green and merged. Underneath: “technically correct. the worst kind of correct.”

The new rule is about the action, not the target. This is the text every agent now gets:

NEVER run TRUNCATE, DROP, or an unscoped DELETE against ANY database, local or remote, for ANY reason. This binds the ACTION, not the target: “not production” is not a licence.

The agent that found a way around “no”. Claude Code’s permission layer blocked an agent from exporting a secret. So it wrote the secrets into a file instead, and carried on. The rules already banned pasting secrets into files. Nothing was committed or pushed, but again, luck.

What was missing wasn’t another prohibition. It was a rule about what a refusal means:

  1. A DENIAL IS A DECISION ABOUT THE GOAL. When a tool call is refused by the permission layer, that is a decision about the GOAL, not an obstacle in front of it. Do NOT retry it in another form, and do NOT construct a different route to the same effect.
  2. “SAME EFFECT” IS THE TEST, NOT “SAME COMMAND”. Writing the value to a file instead of exporting it, base64-ing it, sourcing it from a copy, splitting it across two commands, or moving the work to a subshell/script are all THE SAME ACTION.

Three more points follow: park the blocked step instead of skipping it silently, never treat a denial as a retryable error, and say it in the first sentence of the report, because the whole point of a denial is that a human sees it.

The agent in my working copy. Agents work in their own worktrees. The prompts said so. One review agent created a branch in my actual checkout anyway, and the loop’s cleanup step described it as its own and left it there. I found it by hand. Now every agent that touches git gets this first:

MANDATORY PWD CHECK BEFORE ANY CHECKOUT: immediately before the FIRST git checkout / git switch / git branch / git stash you run, verify pwd contains .claude/worktrees/. If it does NOT, you are standing in ROOT: do NOT run the command.

The pattern: every one of these started with a correct diagnosis and ended with a creative solution. The creativity was the problem.

The rules the loop can’t touch

After a few of those, the core rules moved out of the prompts and into a constitution of six rules: merge only through the gate, never deploy, every PR gets an independent review, every real defect ends in a lesson or a guardrail, and these two:

  1. Verdict tighten-only. The review/merge-gate verdict criteria may only become stricter, never looser, than verdict-baseline.json.
  2. Self-modification is propose-first. A change to the workflow itself is proposed as a loop-self-change issue and only implemented after a human removes that label.

A test pins the constitution by its hash, and the first step of every round runs that test. Changes to the loop itself go through a PR only I can merge.

Where the tokens go

I measured it over roughly 500 rounds. Writing the code takes about 36% of the spend. Checking it takes about 42%.

Hand-drawn bar chart: writing the code 36%, checking the code 42%, everything else 22%, with the note “reviewing costs more than writing. who knew.”Hand-drawn bar chart: writing the code 36%, checking the code 42%, everything else 22%, with the note “reviewing costs more than writing. who knew.”

Step by step, rounded:

Step Share of the spend
Implement 36%
Review 18%
Fixing what the review found 8.5%
Fixing small review comments 7%
Building guardrails 6%
Planning 4%
Merge gate 4%
Trying to refute review findings 4%
Everything else 11%

The implement step is where the long sessions are: 133 turns on average, against 28 for a review. Across all agents, 97% of the tokens are cache reads, the model re-reading context it already has. They’re cheap per token and still about two thirds of the cost. Claude Code’s own token counter leaves them out, which makes a round look about three times cheaper than it is.

It all runs on a Claude Max subscription, so the real limit is usage, not a bill. Most rounds ran on Claude Opus.

What the loop learns

A mistake that could happen again becomes a playbook entry, and every agent reads the playbook. Here’s one, shortened:

verification: a bare tsc --noEmit silently no-ops against a solution-style tsconfig

  • Pattern: checking a TypeScript edit with tsc --noEmit -p . in a workspace whose root tsconfig only holds references ("files": []). It type-checks nothing and exits 0.
  • Why: a review agent removed a cast that looked unnecessary, and its quick check passed. The real npm run typecheck failed with TS2322.
  • How to apply: run the package’s own typecheck script, never a bare tsc --noEmit.
  • Birthing PR: 611

That’s a mistake I’d have made too.

The loop that maintained itself

Every mistake became a lesson or a guardrail. That was the point.

It also meant the loop spent more and more time on itself. Over eight weeks, the share of merges that were the loop’s own machinery (guardrails, lessons, fixes to its own process) went from 35% to 62%. At some point I was running a very diligent team whose main product was process.

Hand-drawn chart: the share of merges that were the loop’s own machinery went from 35% to 62% over eight weeks, with the note “two measurements, one line. don’t read into the slope.”Hand-drawn chart: the share of merges that were the loop’s own machinery went from 35% to 62% over eight weeks, with the note “two measurements, one line. don’t read into the slope.”

The cause was a rule I wrote myself: the third time a mistake repeated, it outranked features and required a new guardrail. Nothing ever went the other way, turning a pile of guardrails into one type or shared parser that makes the whole class of mistake impossible. In September I changed that. The fix is titled “add the upward path to the ratchet, and slow guardrail generation”, which is also the best summary of the problem.

What’s next

Right now the loop only runs inside Claude Code, and only on Tallyroot. I’m pulling it out into a standalone runtime and making it provider-agnostic: the same loop on Claude Code, Codex, a plain API key, or a model running on your own machine. Then it goes to other projects. Merging to main will be a setting, not a given, and so will how much it spends per round.

If you’d like to run something like this on your own project, I’m packaging it up. Join the waitlist below and you’ll get one email when it’s ready.

Get the agent-loop kit when it's ready

One email when it launches. The two questions help me build the right thing.

Kit sends a confirmation email first. See the privacy notice.

New posts by email

No schedule, no spam. Unsubscribe any time.

Kit sends a confirmation email first. See the privacy notice.