Blog / Code an agent can change without breaking it

Code an agent can change without breaking it

Here's a constraint I didn't expect to shape the entire codebase: the primary reader of this code is not a human. It's an agent. Two of them, really: the agent that drives the product through its tools, and the agent that edits the product itself. A lot of deploymill is written, reviewed, and changed by models. And it turns out that writing code for an agent to safely modify is a real discipline, with rules that are sometimes the opposite of what looks elegant to a human.

A dishonest type is a latent bug, not a style nit

The single biggest rule is: everything is typed, honestly, always. No any. Not as any, not Record<string, any>, not an untyped callback parameter sneaking one in. If you don't know the shape of something, you model it, infer it, or mark it unknown and narrow before you touch it.

For a human, a stray any is a small sin. You know what the value is, you'll be careful. For an agent, the type is the documentation it reads to decide what's safe to do. An agent reasons about what a function returns by reading its return type. If that type lies (says a field is always present when it sometimes isn't, or smears any over a real shape), the agent will write confident, plausible, wrong code against it. The type was the safety surface and you sanded it off. So the bar is strict: explicit return types on everything exported, discriminated unions instead of bags of optional fields, | null with a documented meaning instead of a silent ?. Types as load-bearing structure, not decoration.

Small, boring, local

The other half is shape. Small files, one concept each. Boring, explicit control flow over clever one-liners. Organize by domain (put the thing that creates a preview next to the thing that tests it), not by technical layer scattered across controllers/ and services/ and repositories/. Duplicate simple logic until an abstraction is obviously earned rather than extracting it the moment two things rhyme.

This isn't aesthetic preference. An agent edits well when it can understand a change by reading a few nearby files. Every layer of indirection, every clever abstraction, every "you have to know about these five other files" is a place an agent (or a human at 2am) makes a confident wrong guess. Low indirection isn't dumbing it down. It's widening the blast-free radius around a change.

The rules are CI gates, not vibes

A rule that's only in a style doc rots. So all of this is mechanically enforced. The compiler runs with the strict flags turned all the way up, including the ones most projects flinch at, like making every array access yield "value-or-undefined" so you're forced to handle the gap. The linter bans any, bans the non-null ! escape hatch in production code, requires explicit boundary types, and checks that a switch over a union actually handles every case. None of it is advisory. It fails the build.

And there's an ongoing cleanup loop I'm fond of: a workflow that takes a single function and makes it perfect. It tightens its types, fixes its name, strengthens its tests, then updates every caller to match, one function at a time, each its own small reviewed change. It's the opposite of a flag-day refactor. The debt drains a function at a time, and because each step is small and typed and tested, an agent can do it safely on its own.

The payoff loops back to the product

The whole point of deploymill is that an agent can safely operate production. It would be strange, hypocritical even, if the product itself were a codebase an agent couldn't safely change. So the same principle runs all the way down: make the dangerous things unreachable and the safe path the obvious one, for the agent using the tools and for the agent editing the source. Strong types, small files, real gates. Not because it's tidy, but because it's what lets a machine touch this code without breaking it, which is, after all, the entire bet.