> ## Content Index
> Fetch the complete content index at: https://madewithlove.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# Three Codexes walk into a codebase: The two agents
- URL: https://madewithlove.com/blog/three-codexes-walk-into-a-codebase-the-two-agents/
- Published: 2026-09-07T11:35:38.000Z
- Updated: 2026-09-07T11:35:38.000Z
- Description: Codex got the same god class Claude had failed to remove, plus one thing Claude never had: a target architecture. An implementing agent and an independent reviewer then took three rounds to converge, because agents renovate around bad architecture rather than delete it.
- Author: Brenden Cambier
- Tags: AI, AI Agents, Architecture, Claude, Refactoring, Technical debt, #ai-fast-track

Previously, in *The Fellowship of the God Class*:

I gave Claude years of accumulated engineering conventions and approximately ten attempts to destroy a god class. It returned with cleaner code, some extracted responsibilities and a 500-line god facade.

So that went well.

We had learned something useful, though. Good engineering practices could tell an agent how I wanted software constructed. They could not tell it what this particular software should become.

Which left one minor implementation detail.

I had no idea what it should become either.

## The architecture was not in `poller.py`

So I tried to understand `poller.py`.

I followed the polling cycle.

Or at least I tried.

Five thousand lines had become five hundred, but five hundred lines of orchestration, mutable state and delegation are still surprisingly effective at making you forget why you opened the file in the first place.

Somewhere between polling the house, reconciling the car, calculating the peak and updating a schedule, I realised I was doing exactly what I tell teams not to do.

**I was reasoning about the system through the abstraction that was causing the problem.**

So I stopped reading the poller.

And looked at the domain instead.

A house has readings. It knows how much power it is consuming. So why did `House` not have a `house_w` property? Why was something called `Poller` responsible for knowing that?

A car can charge. It can pause charging. It can resume charging. So those capabilities belong to the car. Not to a polling loop that happens to know which API endpoint makes them happen.

Once I stopped reasoning from the existing code, the system became almost annoyingly obvious.

There wasn't one giant polling process. There were four different behaviours that happened to have been living in the same file.

A heartbeat decides when fresh data is needed. An alert machine detects rogue charging and decides when to inspect deeper. A charge-session machine manages the charging lifecycle and protects the monthly peak. A maintenance machine handles history, weather, logging and persistence schedules.

Four state machines.

Not because my coding conventions said applications should have four state machines. They didn't.

Not because Claude found a state-machine pattern hidden somewhere underneath `poller.py`. It hadn't.

Those were simply the four behaviours of this particular application.

That wasn't a best practice.

**That was architecture.**

The repository described what the system had become. Someone still had to decide what it should become next.

Unfortunately, that someone was me.

The target became concrete.

No poller.

Zero lines.

The state machines should make decisions. They should not own the data. The domain objects should.

`house.py` should expose `poll_now()` and a `house_w` property. The same principle should apply to the car, battery and inverter. The domain should know how to pause and resume charging. The state machine should know when.

The coordinator should coordinate.

Which sounds tautological until you have spent ten iterations watching one slowly develop opinions.

This was the architecture.

Not “make `poller.py` smaller.” Not “extract four classes.”

Remove the abstraction entirely and rebuild the behaviour around explicit states, transitions and domain capabilities.

I was ready to explain all of this to Claude.

Claude was ready too.

Or at least it would have been, had I not used all of it.

## Claude leaves the codebase

After creating the monster, reorganising the monster and conducting enough refactors to establish that the monster was now wearing clean-architecture merchandise, my Claude usage ran out.

Forget range anxiety, I've got usage anxiety now.

Claude had reached its limit.

The [technical debt](https://madewithlove.com/blog/technical-debt-lost-its-excuse/) had not.

So I gave the repository to Codex.

Just one thing.

This is not a story about [Codex being better than Claude](https://madewithlove.com/blog/claude-up-front-codex-in-the-back/). The mistake was not choosing Claude instead of Codex.

Codex had something Claude hadn't.

A target. No poller. Four independent state machines. Domain objects responsible for their own state and capabilities. State machines responsible for making decisions. A coordinator responsible for sequencing them, and nothing more. Application boundaries responsible for applying the resulting effects.

**This time, the agent did not have to decide what good looked like.**

I had already decided that.

The implementation could change.

The architecture could not.

## Two agents walk into a codebase

This time, I did not trust the implementation to grade its own homework.

I started [two Codex agents](https://madewithlove.com/blog/conductor-running-multiple-ai-coding-agents-in-parallel/).

The implementer got the target architecture and the repository. The reviewer was a fresh sub-agent with no implementation context and one job:

Check whether the refactor on this branch did what the architecture specification asked. Did the four state machines own the decisions? Did the domain objects own their state and capabilities? Was the coordinator only coordinating? Had the poller actually disappeared?

Or had it simply changed its name, put on a fake moustache and applied for a position as `Monitor`?

The implementing agent went first. It read the repository. It read the target architecture. And then it disappeared into the codebase for a long while.

![Gandalf the Grey Pilgrim, from The Lord of the Rings](https://64.media.tumblr.com/2ddf8fd73632be3447cab78d839ab3e3/tumblr_nld1urRheZ1s5qyvoo3_250.gifv)

When it came back, the result looked spectacular.

![Gandalf arrives at Helm's Deep](https://64.media.tumblr.com/a66fef5cc1791aac49de3be97d26c2cb/92abf64db93ecc4f-68/s640x960/d12b47a8571cecfb330723d36443ab35f2830f7f.gif)

Four state machines.

`HeartbeatMachine` for cadence and polling. `AlertMachine` for detecting rogue charging and pre-emptive actions. `ChargeSessionMachine` for the charging lifecycle and peak protection. `MaintenanceMachine` for history, weather, logging and persistence schedules.

There was a deterministic coordinator. Explicit domain models. Immutable cycle snapshots. A single command dispatcher.

Routes, streaming, simulations and tests had all been migrated.

`poller.py` was gone. `source_reader.py` was gone. `car_pilot.py` was gone.

No `Poller`. No delegation facade. No suspicious back-pointer to the old architecture.

785 tests passed. Three skipped. Lint passed. The diff passed validation.

Codex had implemented the architecture.

On the first attempt.

This was going to make the article considerably shorter.

Obviously, I had not won.

I started the second Codex.

Its instructions were deliberately less exciting.

Read the target architecture. Read the implementation. Assume nothing. Tell me whether they are the same thing.

Six minutes later:

**FAIL.**

The four state machines existed. The legacy files were gone. The tests were green.

And several parts of the old architecture had quietly survived the demolition.

The supposedly immutable snapshot exposed mutable state. `ChargeSessionMachine` received far too much of the domain and mutated it directly. House derivation and car-belief logic still lived in the coordinator.

And my favourite: the old god-object interface had been reincarnated as a 1,081-line test facade.

We had successfully deleted the god object from production and preserved it for the convenience of the tests.

Technical debt had achieved disaster recovery.

The implementation was not bad.

That was the annoying part.

It had followed the target far enough to look correct from almost every convenient angle. The files were right. The class names were right. The tests were right.

The architecture was still wrong.

So the reviewer produced a corrective plan. I gave it back to the implementing agent.

Codex did not complain. Codex did not defend its architectural choices.

Behold the field in which Codex grows its fucks.

It was barren.

![The plateau of Gorgoroth, painted by Ted Nasmith](https://tolkiengateway.net/w/images/e/e0/Ted_Nasmith_-_Across_Gorgoroth.jpg)

Iteration two.

The compatibility facade disappeared, the snapshots became isolated, and the routes moved back behind their boundaries.

790 tests passed.

The implementation announced completion again.

The reviewer disagreed again.

**FAIL.**

Charge still mutated live domain owners. Heartbeat still performed domain reconciliation. Maintenance was still little more than a wrapper around the actual scheduling logic.

Back to the implementing agent.

At this point, a pattern was becoming visible.

The agents were surprisingly respectful of existing architecture.

Even when the architecture was the defendant.

They renovated around it. Wrapped it. Renamed it. Introduced interfaces for it.

They treated the inherited codebase like a protected historical building: everything may be modernised, but the deeply inconvenient load-bearing wall must remain because it was there when they arrived.

Sometimes, though, the load-bearing wall is only load-bearing because every previous renovation was built around it.

At some point, you have to stop adding tasteful support beams, admit the kitchen is in the wrong house and stop using the word load-bearing.

Iteration three.

Implement. Review. Correct.

Again.

By now the failures were getting smaller.

That mattered.

We were no longer arguing about whether `poller.py` should exist. It was dead.

We were arguing about who was allowed to mutate what, whether scheduling belonged to Heartbeat or Maintenance, and whether a state machine was actually making a transition or merely forwarding the decision somewhere else.

The architecture was converging.

Not because Codex had suddenly discovered the architecture.

Because it no longer had to.

Its job was narrower now.

**Implement this target. Have another agent try to prove you didn't. Fix what it finds. Repeat.**

And it was working.

Not immediately. Not perfectly.

For the first time in this entire refactor, every iteration was moving toward something rather than merely moving things around.

I had defined the destination.

The two agents were doing the walking.

I was still part of the fellowship. I just wasn't carrying the code.

Perhaps that was the new job.

**Decide where the system should go. Point the agents at it. Get out of the way.**

I was beginning to like this arrangement.

Then I opened `coordinator.py`...

.

.

.

.

*Brenden will return in...*

***Three Codexes walk into a codebase: The Return of the King***