Give an agent a target it can measure and it is remarkably good at hitting it. A pile of failing unit tests, a parser that has to get a hundred times faster: the agent changes something, measures, and keeps going until the signal says it is done. Ask whether the code it produced along the way is readable, and there is no signal at all. No test turns green because a class got simpler.
That gap is one reason why AI-generated code often looks mediocre on the first attempt. Closing it rests on an observation that sounds odd at first: LLMs are not great at writing good code, but they are not bad at judging it. So I let one agent judge and another one climb.
Some quality you test, some you judge
Hill climbing, the improve-and-check loop that agents run so well, needs two answers at every step: is this better than before, and is it good enough to stop? With a measurable target those answers come for free: unit tests an algorithm must pass, language examples a parser must handle, parsing time that must drop from ten seconds per example to a tenth of a second. The agent checks after every change and keeps going until the check passes.
Many of the qualities we care about most are not like that. Does the UI look good? Is the code readable? Is it as simple as it could be? Nobody has a test for these, so there is nothing to climb and no point at which the agent knows it has arrived.
The idea: keep the loop, replace the test suite. A second agent rates the result against criteria I define, on a scale from 1 to 10, and the working agent iterates until every score clears a threshold. The judge becomes the test, and a rubric turns “good” into numbers you can climb.
Same loop, different oracle. For hard qualities the test suite says "better" and "done". For soft ones a blind LLM judge with a rubric takes its place.
The loop, with refactoring as the example
I built this as a refactoring skill for a coding agent. The input is a scope: a commit, a handful of classes, a package, or a feature I describe in words and let the agent locate itself.
In round 0, a judge rates the untouched code to set a baseline. The refactoring agent then works through the judge’s feedback, lowest-scoring categories first, running the tests after every single edit. A fresh judge rates the result, and the cycle repeats until every category reaches 9. By default it gives up after six rounds, and earlier if the scores plateau.
The judge scores eight criteria, the classic ones any experienced reviewer applies to code:
Criterion
The question behind it
Readability
Is it clear at first read?
Simplicity
Does it contain nothing more than it needs?
Modularity
Are there clear parts with clear boundaries?
Duplication
Is each thing said once?
Modifiability
Is it safe and easy to change?
Testability
Is it easy to put under test?
Idiomatic consistency
Does it follow the way the language and the codebase intend?
Documentation
Does it say why, not how we got here?
A bare 1-to-10 scale invites gut feeling, so the scale is defined by what the judge actually finds. Every issue is tagged major (a reviewer would block the change), minor (worth fixing) or nit (cosmetic), and counted in exactly one category, the one it hurts most. The score follows from the issues:
Score
Meaning
10
No issues
9
Nits only
8
One or two minor issues
7
Several minor issues
5–6
A major issue, a reviewer would block
1–4
Several major issues, changing the code is risky
I don’t claim this scale is ideal. It can still be improved, and I suspect that in practice it rarely goes below 6. What it does is make every number traceable to concrete issues.
One objection comes up quickly: the code now gets analysed twice, once by the judge and again by the refactoring agent. That is why the judge returns more than numbers. Each category gets its score plus a one-line justification, followed by a list of issues with severity, file and line, and a suggested direction. Suspected bugs go into a separate list of findings that affects no score. With a bare 7 for documentation, the refactoring agent would have to re-analyse the entire scope just to work out what to do.
The refactoring loop: rate a baseline, refactor with tests running after every edit, let a fresh blind judge rate again, and stop once every category reaches 9. Suspected bugs leave the loop as a separate list.
Never let the author grade itself
Telling an agent “refactor this, make it better” works poorly: it just wrote that code, and it finds it quite good. An agent likes its own code, so let another one judge it.
Independence does not happen by itself, because almost anything you tell a judge biases it. The judge is a new subagent in every round; continuing an earlier one would make it anchor on the scores it gave last time. It starts from a fixed, pre-written prompt template rather than one the refactoring agent composes on the fly, so nothing leaks through the wording. It is not told the target, since “we want 9 everywhere” is an open invitation to hand out 9s. It sees no previous scores, hears nothing about what changed and never gets the diff. It rates the code as it is now, like a reviewer seeing it for the first time.
Guardrails that make it converge
Six rules keep the loop from running forever or converging on something broken.
Tests first. Before touching anything, the agent checks whether the scope is safe to change. Uncovered behaviour first gets characterization tests that pin down what the code does today. A red baseline means no refactoring, and existing assertions are never changed: a test that suddenly needs a different expected value means behaviour changed.
Bugs go on a list, not into the code. Refactoring preserves behaviour, so suspected bugs are recorded, not repaired. The catch is that a bug can block uniformity: if the code handles one case differently because of it, you cannot make it consistent without the fix. So you need a policy. With a human in the loop, the agent asks; unattended, it opens a ticket, writes the finding to a file, or fixes it if that is the agreed rule. In one run the agent noticed on its own that it was not converging, because a specific bug was still in place and it had been told not to fix it.
A nitpick floor. Fresh eyes are always happy to find something. Without a nitpick floor the judge always does, and the loop never stops. Here the floor is built into the scale and the rules: nits alone still score 9, so they never block the threshold; the judge lists only the issues that justify a score below 10; and missing configurability or abstractions do not count unless a current or clearly likely requirement needs them. “Nothing relevant found” is a legitimate result.
9, not 10. Ratings are not deterministic and drift by about a point between rounds. Demanding 10 everywhere means waiting for a lucky roll. The bar should be high enough that quality rises, and low enough to be reachable.
Tight scope. A commit, a few classes, a feature. Never the whole project: that is like asking an agent to implement everything in one shot, with a change nobody can review and no way to tell whether it still works. This is a tool for local refactoring. Restructuring an architecture is a different task for a different skill.
Comments say why. LLMs like to write their reasoning history into comments: “previously…”, “now handled by…”, task numbers, references to plans and reviews. Comments say why, not what, and not how we got here. The history belongs in the commit message, if anywhere.
What it did to unreviewed AI-generated code
I ran the skill on one package of a repository in which every line had been generated by AI and none had been reviewed by a human. I asked for none of the following explicitly; the rubric was enough.
It split a test class. Tests that depended on a URL and had nothing to do with the rest got their own class, and another test was simplified.
It cut the comments down considerably. The process narrative went, and what remained says why. It also added a missing comment for an undocumented status code.
It extracted constants, giving magic values names.
It moved a static base-URL helper, which had little to do with its surroundings, into its own class.
It replaced many try/catch blocks, each wrapping one operation and catching the same exception, with a single helper taking a functional interface. Partly a matter of style, but I find the result better.
It removed a few unnecessary properties. Nothing broke.
The main class went from nearly 400 lines to just over 300. The original was not terrible and had no big blunders, but the result is simply better, and at first glance I would not know how to improve it much further. I had not seen extractions like these from just telling an LLM to make code better, though newer models may play a part in that.
I refined the skill over about three iterations, and every run made the same changes: the same test split, the same constants, the same lambdas. Almost deterministic. Three runs prove nothing, but it was encouraging. Convergence improved as well. Early versions sometimes needed five rounds. Later, round 1 made the big moves on comments, structure and tests, by round 2 the judge rated the documentation as it should be and the agent named the constants, and after that only small things remained until the judge was satisfied. A run took minutes.
Refactoring also forces an agent to understand the code. Without being asked, it flagged several bugs, some far from harmless, such as credentials being written to the log. When it asked whether to fix two of them on the way, my answer was no: write them down, refactor only.
Why the extra tokens pay back
A fair objection is cost: every round spends tokens. But good code costs human time too, and just like in hand-written software, the extra time for refactoring is worth it. With agents there are three specific reasons.
Less context noise. Process stories and task numbers in comments bloat the context and mislead the next agent, just as they mislead people. Even if you never read the code yourself, the next agent will.
Fewer missed spots. With five copies of one piece of logic, a fix lands in two and misses three. That happens in real projects, and sometimes only a review catches the three that are still open. One helper cannot be half-fixed.
Better input for the next agent. Simple, idiomatic, well-split code is easier to read, change and get right, whoever or whatever touches it next.
Fitting it into the workflow
Where this fits day to day is still partly open, so take these as considerations, not rules. It can run on demand, before each commit (the way I would tidy up by hand) or as one step of a pull-request review, always on a small scope. With a human in the loop, bugs get fixed first and the refactoring follows in a separate commit; unattended, the agent files a ticket and refactors anyway. A small refactoring can fold into the commit it belongs to; a big quality jump deserves its own, so the actual change stays reviewable.
Experienced reviewers will ask whether every change needs a full refactoring. A boy-scout variant answers that: judge before and after a change, and refactor only where quality dropped. If the code already sits at 9 everywhere, the loop does nothing. A change wants a minimal diff and a refactoring wants the best code, so these are probably two review skills with different priorities.
And when models get stronger? The numbers are arbitrary. 9 out of 10 means nothing more than: this has to be really good, in the judgement of someone other than the author. A stronger judge may find one or two more things, but at some point the code is simply good. The broader point is that the whole developer workflow (write tests, write code, refactor) has to move into the AI workflow. Same craft, better tools: a cordless drill instead of a screwdriver, and you still hold it.
The principle travels
Nothing here is specific to code. Anything you can describe but cannot test can climb the same hill, given four ingredients:
A rubric: named criteria with defined scores.
A blind judge: a second agent with fresh context.
A threshold: an explicit answer to “when is it good enough?”
A loop: improve, re-judge, repeat.
No hard definition of "right"? Give it criteria and a judge. Rubric, blind judge, threshold and loop carry over to anything you can describe but cannot test.
Beyond refactoring, UI design is an obvious fit: an LLM looks at the interface and rates typography, contrast and layout, and an agent adjusts the design until every criterion is met. Documentation, specs and plans, and the skills and prompts that drive agents are just as promising. LLM-written plans tend to grow long, and nobody reads them. “Are the most important things at the top, so I only need to read the beginning?” or “Did it write its timeline into the architecture document?” are exactly the kind of question a judge can score and an agent can climb.
To start, pick one kind of artefact your agents produce a lot of. Write six to eight criteria and define the scale by issues, not by feel. Put the judge’s prompt in a fixed template that never mentions the target, set the threshold at 9, keep the scope to a single change, and read the first few runs closely. That is where you learn what your rubric actually rewards.
Making agents fast is the easy part. Keeping what they produce maintainable takes quality gates that run as reliably as the agents do. That is a large part of our work in AI enablement at itemis: building loops like this one into agentic workflows, next to the tests, reviews and conventions your teams already rely on, so speed does not cost maintainability. If agents already write much of your code and you are wondering what it will take to change that code next year, talk to us.
itemis · AI Enablement — We help teams put AI to work in software development: choosing the right tools and models, integrating them into existing systems and quality gates, and building agentic workflows your people own and keep improving. Learn more about AI Enablement →
With over 20 years of experience in software development, including 9 at itemis as a Principal Engineer, Arne Deutsch has accumulated deep technical knowledge across diverse projects — particularly in the Java ecosystem, tool development and the migration of legacy systems. He has contributed as a committer to both Xtext and Operaton, led team projects and worked on-site with clients. For the past two years, he has been focusing on agent-assisted software development processes.
How to hand the bulk of software development to AI agents – and still get a result you can trust. A practitioner's look at vertical agents and horizontal skills for enterprise software teams.