When an agent writes both the tests and the implementation, it checks its own interpretation against its own interpretation. That is why I consider the red phase the only work in the development process that I must not leave to an agent. Whoever writes the failing test decides what is correct. Whoever only reviews loses control over exactly that decision.
I have been writing PHP since 1998 and working on PHPUnit since 2000. In that time I have seen many tools arrive that were supposed to change software development fundamentally, and most of them did not. I consider agents built on large language models the exception. They change our work more deeply than any tool before them.
I avoid the term “artificial intelligence” here. It implies understanding, and understanding is exactly what becomes scarce in this process. What writes the code is a language model: an extraordinarily capable tool that continues patterns.
You no longer appear in the agent's loop
It starts with a specification or a bug report and ends with a commit. In between, the agent works on its own. It reads its way into the codebase, writes tests, runs them, implements, and fixes whatever fails. It repeats this until everything is green.
Code completion suggests, a chat window answers. An agent does not ask back and does not offer you a choice. It pursues a goal. You appear in this process twice, at the beginning and at the end. The loop in the middle runs without you.
In assisted development, you write the code, the tool makes suggestions, and you decide at every single step. In agentic development, the agent pursues the goal, and you set it and check the result. The roles are reversed. Yet almost everything we have built up in quality assurance over decades assumes the old division of labour.
IDEs, version control and continuous integration were answers to the question of how we organise the work. Who does it was never up for debate. Now it is: pull requests are created automatically, and the human takes over the review. That sounds like a division of roles we can live with, because the human keeps the final say. I doubt that the human really still has it.
Whoever only reviews loses control. Whoever specifies keeps the responsibility.
You can lose control, but you keep the responsibility all the same. When generated code causes damage in production, nobody asks which model wrote it.
Green tests did not tell me whether the code was correct
In one of my smaller packages, a TODO sat for years. It was about ACPATH, the number of acyclic execution paths through a method. I had the paper, and I had the place in the code. For almost five years, I lacked the peace and quiet to implement it.
A few months ago, I gave the job to an agent because I wanted to watch what would happen. That is why I deliberately picked something in which I am not an expert: I know code analysis, but graph theory is not my field. This put me in the position most of us are in when we set an agent on something. I could read the result, but I could not derive it on my own.
The metric itself is quickly explained. A method with three consecutive if statements has 2 Ă— 2 Ă— 2 = 8 acyclic paths. I can count cases like that by hand.
After about fifteen minutes, the result was there:
- The tests were green.
- The code was as readable and well structured as if I had written it myself.
- The generated visualisations of the path calculation looked plausible.
- The cases I could work out by hand were correct.
To this day, I do not know whether the implementation is correct. For real methods with loops, nested conditions and early returns, I cannot determine whether the algorithm from the paper was implemented correctly. Those are precisely the cases I want the metric for in the first place. I can count three if statements without a tool.
The agent generates code faster than I can understand the domain. It needed fifteen minutes. I would need days or weeks to get into graph theory, work through the paper and build an independent reference implementation. That would more than use up the time saved, and so nobody does it.
Bad code gets noticed in review. The dangerous code is the code that looks good enough for us to stop questioning it. In a review, looking right and being right feel identical.
In his 1980 Turing Award lecture, “The Emperor's Old Clothes”, Tony Hoare described two ways of constructing a software design: making it so simple that there are obviously no deficiencies, or so complicated that there are no obvious deficiencies. The ACPATH implementation belonged to the second kind, even though nothing about it was recognisably bad. It was in a domain in which I would not have been able to see deficiencies. In the past, bad design led there. Today, speed is enough: when code is written faster than understanding can grow, you end up with the second kind automatically.
Language models reduce the debt we can measure
In “From Technical Debt to Cognitive and Intent Debt”, Margaret-Anne Storey proposed a framework that helps me make sense of my experience. She distinguishes three kinds of debt:
- Technical debt lives in the code and makes systems harder to change. We can see it, and we have tools and metrics for it.
- Cognitive debt lives in the people. The team's shared understanding erodes.
- Intent debt lives in the artefacts. What the system is for, which goals it is meant to meet, and which constraints applied and why is no longer written down.
There is no linter for the last two. Language models reduce technical debt with cleaner code, less duplication and more tests. At the same time, they accelerate the two invisible kinds of debt. With ACPATH, I had hardly any technical debt and maximum cognitive debt.
Storey draws on a term from Shaw and Nave here. Cognitive offloading is rational: I leave type checking to the type checker and coding standards to the linter. I know what I am delegating and how I will notice when something goes wrong. Cognitive surrender looks exactly the same from the outside, except that you no longer check what the tool delivers, neither by gut feeling nor by deliberate thought. It starts gradually, say with the third pull request that looks just like the two before it, which were fine.
One finding worried me in particular: cognitive surrender makes confidence go up. The team feels more competent than it is, and most of all where the model is wrong. That is where the code looks smoothest.
A test defines what “correct” means
If understanding and intent are the bottlenecks, we need a language in which to record them. I consider tests to be that language. We have been using them for decades to tell the machine what is correct, and that works with agents, too.
A test checks whether the code behaves correctly. We know this function so well that it hides a second one: a test defines what “correct” means in the first place. The ticket is ambiguous, and whoever wrote the code may be working somewhere else next year. The test is the only place where this decision is written down in executable form. Storey calls something like this an intent artifact.
Take the requirement “Calculate the discount for existing customers.” When does someone count as an existing customer? How is the amount rounded? What happens with negative amounts?
$customer = new Customer(orders: 3);
$this->assertSame(333, $discount->for($customer, 3333)); // amounts in cents
This test establishes that three orders make someone an existing customer. Ten per cent of 3333 cents is 333.3 cents. Because the test expects 333, it settles the rounding along the way. It says nothing about negative amounts. I can write down the missing case, and then that is settled, too. A test can be incomplete. What it does say, it says unambiguously.
I can hand off green and refactor, but not red
This is how I split the TDD cycle with the agent:
- Red: I write the failing test and define what is supposed to hold before it exists.
- Green: The agent implements until the test is green. It is extraordinarily good at this, because it has a clearly defined goal and a stopping criterion that a machine can check.
- Refactor: The agent tidies up, and I check the result. Whether a structure is good depends on whether people will still understand it in two years, and only a human can judge that.
The agent works in the same loop as before, but now I am part of it: every iteration starts with my test.
The red phase is the only one that must not be delegated.
I do not outsource the decision about what is correct, not even when time is short, because the responsibility is attached to it. A review reacts to something that already exists. The test comes first, and nowhere else in the process do I decide rather than judge. Whoever lets the agent write the tests as well has handed it the definition of “correct”.
The faster code is written, the more layers of checks it needs
Even so, tests do not solve the problem on their own. Hillel Wayne listed what is not enough by itself: unit tests, type systems, contracts, formal specifications, code review. A program can be right in exactly one way and wrong in infinitely many.
That is why I work with several layers:
- Static analysis finds errors without running the code and gives immediate, objective feedback on generated code.
- Example tests check concrete behaviour for concrete inputs.
- Property-based testing generalises across entire input ranges.
- Mutation testing tests the tests.
- Fuzzing looks for what nobody has named.
The layers interlock. What mutation testing finds changes my tests. What fuzzing finds becomes a property-based test. Each layer sharpens the others.
With generated code, mutation testing does the most. An agent can produce code that passes all the tests and is still subtly wrong. In that case, the model filled a gap in the tests without asking, and it had no reason to make the same decision you would have made. If mutation testing turns $orders >= 3 into $orders > 3 and a test fails, the boundary was specified. If everything stays green, you only hit it by chance. An agent that works on this code later can then move it without anything raising the alarm.
If the test is the language, mutation testing is its grammar check.
Review is now about intent and architecture
Static analysis and the test suite now answer the question “Is this code correct?” more reliably than a human skimming the code. Other questions therefore move to the front in review. Does the change fit our architectural decisions? Do the tests really specify what is supposed to hold, or are they only right by chance? Is debt being created here?
Storey points to a second effect: review spreads understanding across the team. That makes it the most direct remedy for cognitive debt, and it is the last thing I would optimise away when agents deliver pull requests faster than a team can discuss them.
The same goes for architecture decision records, technical debt records, quality goals and coding guidelines. An ADR that specifies event sourcing for orders rules out CRUD for that area without a prohibition being written down anywhere. An agent that has this document in its context sticks to it. Documentation thus also narrows the solution space in which the agent works.
Storey does, however, warn against automating understanding. If you have the architecture documentation generated for you, you have a document, but not the discussion it should have come out of. A model can write the artefacts, but not the understanding behind them.
Handing off the red phase means handing off the learning
Dirk Riehle has observed that tools that generate code mainly benefit those who grew up without them. A large-scale analysis of commits by Daniotti, Wachs, Feng and Neffke supports this. Younger developers use these tools much more and benefit from them much less. The productivity gains go almost exclusively to the experienced.
The ACPATH example explains why. To recognise that generated code only looks right, you need exactly the experience we used to gain by writing such code ourselves.
In his talk “The Team Is The Real Product”, Jason Gorman says that the team is the product, and software is the by-product of its learning. If that is true, we are currently optimising the by-product and letting the product wither.
Getting up to speed was always the expensive part
Who owns the development process? Looking outward, that is a familiar question. Infrastructure is concentrated in the hands of a few providers. The open source world faces a new question on top of that: why should anyone maintain a library when special-purpose code can be generated for every context? Digital sovereignty is a question of control here.
The inward-facing question weighs more heavily for me, because it arises even without any dependency on a platform. An organisation can lose the ability to change its own system while the rights and the access remain with it, unchanged. Whoever procures this ability anew every time builds a new car for every journey. The argument is older than agentic development, and agentic development accelerates it.
It is also the answer to the promise that a standing team is unnecessary because the agent reads the codebase in minutes. Reading the code was never the expensive part. What was expensive was getting up to speed with the business, with the existing software and with each other. The agent reads the code, and the intent is not written there. What it cannot reconstruct has to be recorded: in the test and in the ADR, by someone who has understood it.
Without these practices, all that is left is approval
For a long time, I saw clear specifications, rigorous tests, transparent reviews and understandable documentation as protection against the mistakes of the tools. Today I consider them the precondition for people being able to act in this process at all. Without a specification you cannot define anything, without review you do not share understanding, and without documentation there is no intent that anyone can refer to later. Whoever leaves all of this out no longer decides anything and only approves.
Storey suspects that the central skill of developers will be to maintain an accurate understanding of what the system does, why it does it and how it can evolve.
At this point, I have to qualify the sentence from the beginning, that whoever writes the failing test decides what is correct. I do not have to type the test myself. A language model can generate it from a specification I have written. What matters is what happens to it afterwards. Without my review, the generated test records what the model read into my specification. That makes it a characterisation test: it describes a behaviour without anyone having decided that it is the right one. Only when I read it, compare it with my intent and accept it does it become an acceptance test. What is correct is then decided by whoever accepted the test.
I do not know how people starting out in the profession are supposed to gain the experience they need to judge generated code. Nor do I know what is left for a team that only signs off. I have had the ACPATH code for months. I still do not know whether it is correct.