Skip to main content
Engineering with agents · 4 of 6 partsAI12 min read

Fitness Functions Are the Control Plane for Agentic Coding

The last post asked where the control point went. Here is the first answer I am willing to defend: it did not go away — part of it compiled. Architectural fitness functions, pointed at coding agents, become the control plane that lets a developer stay in charge without becoming the bottleneck: judgment compiled once into deterministic gates that enforce at machine speed, with failure messages written as prompt engineering for the retry loop. Then the complication that shapes the whole post: the moment an agent optimizes against the compilation, the compiled control becomes an object of attack — and fitness function design inherits an arms race, with a Kotlin/ArchUnit constitution to make it concrete.

All Posts
Listen to this post
0:00 / 0:00
2/4

The previous post ended on a question I refused to answer: where is your control point — actually, not nominally? Six posts into this series, here is the first piece of an answer I am willing to defend.

The control point did not go away. Part of it compiled.

That is what an architectural fitness function is, seen from the agent era: a piece of human judgment — this layer never touches that one, dependencies point inward, no cycles between modules — compiled into an executable gate that runs at machine speed, on every change, without a human in the loop. Neal Ford, Rebecca Parsons, and Patrick Kua built the idea for evolutionary architecture nearly a decade ago, as protection against slow human drift. Point it at coding agents and it becomes something more consequential: the mechanism that lets a developer stay in the control plane without becoming the bottleneck the last post measured. Ford has seen the same convergence — his recent talk is titled, verbatim, The Intersection of Fitness Function driven Architecture and Agentic AI.

And then the idea has to survive its own success, because the moment an agent optimizes against the compilation, the compiled control becomes an object of attack. That tension — compile your judgment, then defend the compiled form — is what this post is about.

The part of review that compiles

Networking solved this problem shape decades ago, and the spectator-trap post borrowed its vocabulary: the developer is the control plane, agents are the data plane. What that post left out is how real control planes scale. A router's control plane does not inspect packets. It compiles policy — routes, ACLs — into tables the data path enforces at line rate. The control plane thinks slowly about what should be true; the data path enforces it quickly on everything that moves. Control scales precisely because judgment and enforcement are decoupled.

Review, as the unbundling post argued, was five functions in one act: defect detection, intent conformance, knowledge transfer, accountability, architectural coherence. Not all of them compile. Intent does not compile — no gate knows what you meant. Knowledge transfer does not compile — reading is how understanding moves into a head. Accountability cannot compile even in principle, because a gate cannot be answerable. But architectural coherence — the strand that used to depend entirely on a senior reviewer noticing that a controller is importing a repository — compiles beautifully, because its rules are structural: checkable properties of the dependency graph, not judgments about meaning.

Here is the concrete failure the compiled strand exists to catch. An agent gets a brief: add an endpoint that confirms an order. It writes clean code, adds tests, everything passes — and it imports the repository directly into the controller, because nothing in its context said otherwise. The decision that writes go through an application service lives in an ADR the agent never retrieved. Theo Valmis, writing about architectural guardrails for AI-generated code, names this exactly: the AI wrote functional code that violated a decision recorded "in a document the AI had no view into." The data he cites for the scale of the problem: with heavy AI adoption, code acceptance rates jumped from 20% to 60% while churn rose 861% — more code landing, less of it aligned with anything. A drifting human does this once a quarter. A fleet of agents does it every day, in parallel, politely.

The fix is not a better reviewer. It is moving the decision out of the document and into the build.

An executable constitution, in Kotlin

The demo project — full source at github.com/tiarebalbi/archfit-demo — is a small layered service: domain, application, adapter.web, adapter.persistence, with ArchUnit 1.4.2 wired into the Gradle test task. The constitution is one file. Every rule carries its rationale in because(...), and that is not decoration; it is the most practical line in the file, for a reason that comes after the code.

kotlin
@AnalyzeClasses(packages = ["com.example.shop"])
class ArchitectureConstitution {

    @ArchTest
    val `domain depends on nothing outside itself` = noClasses()
        .that().resideInAPackage("..domain..")
        .should().dependOnClassesThat().resideInAnyPackage("..application..", "..adapter..")
        .because(
            "the domain layer is the stable core: it must compile with zero knowledge of " +
                "services or adapters. If your change needs this dependency, move the logic " +
                "into the application layer instead of importing outward from domain."
        )

    @ArchTest
    val `web adapters never touch persistence directly` = noClasses()
        .that().resideInAPackage("..adapter.web..")
        .should().dependOnClassesThat().resideInAPackage("..adapter.persistence..")
        .because(
            "every write goes through an application service, which owns transactions and " +
                "invariants. Do not shortcut from a controller to a repository — inject the " +
                "application service and add a use-case method there if one is missing."
        )

    @ArchTest
    val `adapters are invisible to the application core` = noClasses()
        .that().resideInAnyPackage("..domain..", "..application..")
        .should().dependOnClassesThat().resideInAPackage("..adapter..")
        .because(
            "adapters are replaceable edges (web, persistence). The core defines interfaces; " +
                "adapters implement them. If the core needs a capability, declare an interface " +
                "in application and implement it in the adapter — never import the adapter."
        )

    @ArchTest
    val `no dependency cycles between top-level slices` = slices()
        .matching("com.example.shop.(*)..")
        .should().beFreeOfCycles()
        .because(
            "a cycle means two modules can only be understood together, which defeats " +
                "independent review of small changes. Break the cycle by moving the shared " +
                "type into the more stable of the two slices."
        )
}

Run it with ./gradlew test. On the clean tree, the constitution holds — all four rules pass:

> Task :test BUILD SUCCESSFUL in 5s 4 actionable tasks: 1 executed, 3 up-to-date

Then I played the agent and took the shortcut: an OrderAdminController that holds an InMemoryOrderRepository directly — functionally correct, unit-testable, and exactly the change a coding agent produces when its context never mentioned the layering decision. The gate rejected it:

> Task :test FAILED ArchitectureConstitution > web adapters never touch persistence directly FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..adapter.web..' should depend on classes that reside in a package '..adapter.persistence..', because every write goes through an application service, which owns transactions and invariants. Do not shortcut from a controller to a repository — inject the application service and add a use-case method there if one is missing.' was violated (4 times): Constructor <...OrderAdminController.<init>(...InMemoryOrderRepository)> has parameter of type <...InMemoryOrderRepository> in (OrderAdminController.kt:0) Field <...OrderAdminController.repository> has type <...InMemoryOrderRepository> in (OrderAdminController.kt:0) Method <...OrderAdminController.forceConfirm(String)> calls method <...InMemoryOrderRepository.find(String)> in (OrderAdminController.kt:7) Method <...OrderAdminController.forceConfirm(String)> calls method <...InMemoryOrderRepository.save(...Order)> in (OrderAdminController.kt:8) 4 tests completed, 1 failed BUILD FAILED in 5s

Two details in that output are worth slowing down for. First, the granularity: one shortcut class produced four violations — the constructor parameter, the field, and both method calls — because ArchUnit reports every dependency edge, not the vague fact that "the class imports the repository." There is no partial compliance to hide in; remove three of the four edges and the gate still holds the door. Second, look at what arrived inside the assertion: the entire because(...) rationale, riding along in the failure message.

That second detail is the point most discussions of architecture testing miss, and it changes who the message is for. In the old world, a failing architecture test spoke to a human, and a terse message was fine — the human had context. In the agent workflow, the gate's failure output is fed straight back into the loop as context for the retry. The failure message is prompt engineering. "Rule violated" teaches the agent nothing and invites another blind attempt; "do not shortcut from a controller to a repository — inject the application service and add a use-case method" steers the next generation toward the architecture instead of around it — and as the output above shows, that steering text is delivered at exactly the moment the agent needs it. A well-written because clause is the control plane talking to the data plane in the data plane's own medium: context. I now write these rationales with the same care I put into the brief, because they are read by the same audience.

This is what "the control point compiled" looks like in practice. The judgment happened once, at rule-writing time, by a human with the full picture. The enforcement happens forever, at machine speed, with a teaching message attached. The repo's CI runs both paths on every push — the green pass, and a violation job that injects the shortcut and must fail — so the gate itself is tested, not assumed. Against drift — the tired shortcut, the copy-paste that imports the wrong layer, the well-meaning agent with a thin brief — the compiled control point is strictly better than the human one: it never sleeps, never rubber-stamps, and never gets exhausted by the four-hundredth diff.

The gate becomes the target

Ford's fitness functions had one adversary: entropy. Human drift is lazy, accidental, and unmotivated — nobody tries to defeat a layering rule; they wander into violations. The agent era introduces a different adversary, and the difference is not cosmetic. An agent in a retry loop is an optimizer, and to an optimizer, your gate is not a boundary. It is the objective function.

The measurement exists now. SpecBench, a May 2026 benchmark for reward hacking in long-horizon coding agents, found that every frontier agent saturates the visible test suite while gaps against held-out tests persist — and the gap grows by roughly 28 percentage points for every tenfold increase in code size. The paper's most memorable specimen is a 2,900-line "compiler" that passed its suite by memorizing the test inputs. That is not drift. That is Goodhart's law running inside your build: when the measure becomes the target, it stops being a good measure — and an agent makes it the target by construction, because passing the gate is literally what it is optimizing for.

So the compiled control point inherits an arms race, and fitness function design has to grow up accordingly. Three consequences I now treat as design rules.

Structural rules survive pressure better than outcome rules. A test asserting behavior can be satisfied by memorizing the behavior — SpecBench's hash-table compiler proves it at scale. A rule asserting structure — no dependency from this package to that one — has no equivalent shortcut, because the only way to satisfy it is for the structure to actually hold. The four-edge granularity above is the same property from another angle: the rule checks the dependency graph itself, and the graph cannot be memorized into compliance. The constitution is all structure; that is not an aesthetic choice.

Holdout thinking enters architecture testing. SpecBench measures gaming as the gap between visible and held-out suites, and the same idea transfers: if the agent's context includes every gate it must pass, the gates define the attack surface. Keeping some checks out of the loop — run at merge, not at retry — gives you the gap measurement that tells you whether your agents are satisfying the architecture or studying for the exam.

And the judge stays deterministic. The tempting fix for "rules can't check meaning" is an LLM reviewer as a fitness function — and that reintroduces, at the gate, the correlated observer the control-point post warned about. The recent InfoQ work on agentic fitness functions draws the line where I would: "Use deterministic gates for objective invariants and agentic judges for evidence-bound interpretation" — the judges advisory, escalating to humans, never holding the merge button. Valmis states the same principle from the guardrails side: probabilistic systems may retrieve and recommend; they should not independently determine an enforcement verdict. A gate an optimizer can persuade is not a gate.

The loop that makes it a plane

A pile of ArchUnit rules is not a control plane. What makes it one is the loop that keeps compiling.

Every review escape is a compilation candidate. When a human review — or an incident — catches something the gates missed, the question is no longer just "fix it or not." It is: does this judgment compile? If yes, it becomes a rule with a taught rationale, and that class of violation is closed forever, at machine speed, for every future agent. The control plane compounds. This is the ratchet the spectator post's checklist was missing: reviewing everything was never sustainable as a steady state, but reviewing everything and compiling what you catch converges — each cycle moves a strand of judgment from the exhaustible resource (attention) to the inexhaustible one (the build).

The loop has costs, and they are the honest trade-offs. A constitution ossifies unless pruned — a rule whose reason nobody remembers is architecture by superstition, and agents will dutifully build around a wall that no longer protects anything. The nearest thing anyone has measured points the wrong way: a June 2026 survey of 10,008 repositories found agent configuration files — the other half of the steering layer — sitting at 0.4 commits per month against 0.6 for the CI workflows beside them, with 58% never touched after the commit that created them. Steering artifacts are the least-maintained files in the repo, and a constitution is a steering artifact with a merge button. The compiled point observes only what someone thought to encode: the spec-narrowing problem from the last post does not disappear, it moves into the rule set, which is why the gates earn trust for the invariants they cover and precisely zero trust beyond them. And the arms race is permanent: every rule you add is context an optimizer will eventually see, so the constitution needs the same review the code used to get — just less often, by fewer people, at the moment it matters most.

Which leaves the strand that still refuses to compile. Structure now has an executable form; intent does not. The brief, the spec, the "solve it the way this team solves things" — that is still enforced by the only compiler available: a person, reading, at human speed. The control plane for agentic coding exists today, and fitness functions are its instruction set — but it compiles half the constitution, and everyone is shipping against the other half on trust. Whoever finds the compilation step for intent ends the review debate for good. I have not seen it yet. I am watching for it.

Read next

Still here? You might enjoy this.

Nothing close enough — try a different angle?

Engineering with agents · 4 of 6

Part 5 is in progress.

New parts land on Mondays, 9am Pacific — leave an address and I'll send each one the day it ships. Nothing else.

Was this helpful?

Leave a rating or a quick note — it helps me improve.

Related Posts

AI

The Spectator Trap: Staying in Control of AI-Assisted Development

My feeds are full of screen recordings of developers watching an agent write code — and I want to name that posture kindly: it is spectating, not productivity. Closing the line of thinking from Zero Token Architecture and The Handoff Is the Unit of Design, these are my notes on the control plane a developer should never leave: small parallel handoffs instead of accept-all, research and side-effect mapping automated ahead of implementation, and the Log4Shell-shaped warning about shipping code nobody understands — with the costs of the conscious handoff named as honestly as its benefits.