Sunday morning. I’d just discovered Fred Again’s music, and while it played I caught myself thinking: I like this — it’s creative, there’s something new in it. It left me inspired, itching to try something new with my own agents. A quick dig through my notes, a flurry of ideas, and in the time it took to read one article and listen to half an album, I landed on a question: why does the second agent always end up doing the same thing — evaluating?
The industry has spent the last year refining the same loop. Generator-Verifier, Maker-Checker, Actor-Critic, Reflection Loop. Different names, same idea: one agent generates, another evaluates, the first one corrects. Microsoft calls it Maker-Checker in Azure Architecture. The RL community calls it Generator-Verifier. ASDLC.io formalized it as Adversarial Code Review with Context Gates. All well documented, all very standard.
But all these patterns share one assumption: the second agent is an evaluator. It reads the code, compares it against the spec, and gives its opinion. It’s a judge.
What if instead of a judge, you put someone whose only job is to destroy what the first one built?
I call it the Angel-Devil pattern. The Angel (builder) constructs. The Devil (breaker) tries to break what the Angel built. What survives is what you ship. I wanted to know if this difference, which sounds obvious, actually produces different results from the classic judge.
The experiment
I built a minimal harness in TypeScript. Three agents, same model (Kimi K2.7 Code), same spec, same default sampling settings. The only variable is the system prompt:
- Builder: receives a spec, generates the implementation.
- Judge: receives the spec + the code, evaluates whether it meets the requirements. Produces a verdict (APPROVED / NEEDS_CHANGES / BLOCKED) with evidence.
- Breaker: receives the spec + the code, constructs specific inputs designed to crash it, produce wrong output, or violate the spec. Every attack must cite the root cause in the code.
I ran this against three tasks: a dynamic form generator (vanilla HTML/JS), a deep merge function, and a URL router. Each task was chosen because it has non-obvious edge cases where “looks correct” and “is correct” diverge.
The judge and breaker never see each other’s output. They work independently on the same code. I’m the final evaluator.
A caveat before going further: this is an exploratory experiment, not a benchmark. One run per task, one model, no fixed seed, no repetition. The results aren’t deterministic and I’m not claiming they are. I’m not after a metric — I’m after whether the breaker and the judge reason differently about the same code.
What happened
Dynamic Form
The builder generated a form component that renders fields from a JSON config, handles conditional visibility, and validates on submit. Clean code, well structured.
The judge found 3 issues (2 real bugs + 1 stylistic nit). Verdict: NEEDS_CHANGES.
The breaker found 5 attacks (5 real bugs, 0 padding). Three of them were invisible to the judge:
- A select field without
optionscrashes withTypeErroron render - Validation errors persist visually when a field becomes disabled
- Strict equality in
visibleWhencauses"42" === 42to silently fail
Deep Merge
This is where it got interesting. The judge marked “unlimited depth” as PASS, arguing “depth is limited only by the JS stack.” The breaker generated a 200k-level nested object and showed it crashes with RangeError: Maximum call stack size exceeded. The judge literally approved — with cited evidence — a feature that fails.
The array case is subtler. The judge marked “arrays replace target arrays” as PASS — and that’s true, they do replace — but it never noticed the replacement returns the source array by reference, so mutating the output mutates the input. To be fair: the judge did catch the reference-aliasing problem in general (it failed the “returns a new object” criterion); it just never connected it to the array criterion. The breaker went straight to the concrete case.
URL Router
The most balanced result, and the one that most tempers the thesis. Here the judge and breaker converged on the serious stuff: both found the malformed percent-encoding crashes and the stale param-name bug on route re-registration. The difference was marginal — the judge found one issue the breaker missed (query strings not stripped from paths), and the breaker found one the judge approved as PASS (registering a route with a falsy handler — null, 0, "" — makes it unmatchable, because the code uses if (node.handler)). On this task, a breaker wouldn’t have bought you much over a good judge.
The pattern
Across all three experiments, one thing was consistent: the breaker found bugs that the judge explicitly approved as correct.
This is not about finding more bugs. The judge finds real issues too. The difference is in the type of reasoning:
- The judge asks: “does this code satisfy each requirement?” It reads the code, traces the logic, and evaluates.
- The breaker asks: “what input would make this code fail?” It imagines adversarial scenarios and traces what would actually happen.
These are different cognitive operations. The judge catches spec non-compliance. The breaker catches the bugs that ship to production.
Prior work
This experiment doesn’t exist in a vacuum. There’s relevant work worth mentioning:
ASDLC.io defines the “Adversarial Code Review” pattern as a formal practice with three-tier Context Gates. It’s a solid theoretical and methodological framework, but it doesn’t include the empirical comparison I made here.
Refute-or-Promote (arXiv, April 2026) is probably the closest paper. They propose an adversarial multi-agent system with kill mandates and empirical validation for defect discovery. It runs in the opposite direction to mine — they kill false positives (reported bugs that don’t exist), I chase false negatives (real bugs the judge approves) — but it validates the underlying principle: an empirical test beats opinion, even unanimous opinion. Their most instructive example: ten reviewers unanimously endorsed a non-existent vulnerability in OpenSSL, and it was killed only by a single empirical test.
adversarial-review (GitHub) uses Claude + GPT Codex in a multi-round adversarial debate, but those are two judges debating, not a judge against a breaker.
RedCoder (arXiv, July 2025) is a red-teaming framework for Code LLMs with an attacker, defender, and evaluator, but its focus is making the model generate vulnerable code, not evaluating already-generated code.
My contribution is the direct, controlled comparison: same model, same code, judge vs breaker, concrete data. As far as I’ve seen, nobody has published that.
What this means for agent architecture
Generator-Verifier, Maker-Checker, Actor-Critic. They all put an evaluator after the generator. The evaluator reads, opines, suggests. It’s the Angel talking to another Angel.
The Angel-Devil pattern changes the dynamic. It’s not “generate and evaluate”. It’s “generate and try to destroy”. The Devil doesn’t suggest improvements. It fabricates malicious inputs and tells you exactly which line of your code explodes. It’s a fundamentally different kind of feedback: harder to dismiss because it comes with a concrete failing test case, not an opinion.
The practical implication is simple: if you’re building coding agents for production, an Angel alone is not enough. You need a Devil integrated into the generation loop. Not as a separate QA step, but as part of the cycle: build, attack, survive, ship.
The code
The entire harness is about 100 lines of TypeScript. Three system prompts, a sequential orchestrator, and a spec file per task. No frameworks, no dependencies beyond the OpenAI SDK. Adding a new task is creating a markdown file.
Everything is open source: github.com/aleksandarlabs/adversarial-harness
What’s next
This was a static analysis experiment. Neither the judge nor the breaker executed any code. The next version adds E2B sandboxed execution so the breaker’s attacks run automatically against the builder’s output, producing pass/fail evidence instead of hypothetical analysis. That closes the loop: the builder receives concrete test failures, not prose feedback.
I’m also curious about the iteration dynamics. Does the builder produce better corrections when it receives “your code crashes with this input” versus “your code doesn’t fully satisfy requirement #3”? That’s the next experiment.
But the question that really keeps me up at night is a different one: what if it’s not Angel or Devil, but all three? The Angel builds, the Devil attacks, the Judge evaluates the evidence from both. Three perspectives on the same code, three types of feedback, one loop that combines spec compliance with adversarial robustness. That’s no longer a review pattern. It’s an immune system for code.
I know, I got a bit carried away with that last one. But the idea stands.