All posts

Devlog

The codebase out-votes the prompt — TigerStyle that agents can't ignore

I broke my own rule in four places at once. It was written down twice — once in the architecture notes, once in the house rules every agent reads before it touches anything — and eight agents working in parallel broke it anyway.

Nothing noticed for days. The check that would have caught it is the last step of the full verification pass, and the fast loop everyone actually runs does not include it. So the broken artifacts sat there, committed, green by every signal anyone was looking at.

The standard is not mine. TigerStyle is TigerBeetle's — a coding guide written for a financial database, where being wrong is not survivable. Most of it moves to a game engine unchanged: a simulation that must not miss a frame and a ledger that must not lose a cent turn out to want the same things. Four of its rules do the heavy lifting here, and this post is about what it took to make each one hold.

ZERO COPYone memory,no marshallingONE VOICEone naming,zero warningsASSERTentry, return,after the callALLOCATEonce at boot,never againEVERY ONE OF THEM FAILS THE BUILD WHEN BROKEN
The four rules that carry this codebase — and the one property that makes them real.
  • The original — TigerStyle, from the people who wrote it — worth reading whether or not you build databases. https://tigerstyle.dev/

Written down twice. Broken anyway.

The rule lived in two places: the architecture notes that explain why it exists, and the house-rules file every agent reads before it writes a line. It was not buried, not implied, not a matter of taste. One sentence, stated twice — and six modules got it wrong on the same afternoon.

What it forbids is small. A few modules here borrow memory they do not own: they are handed a window into another component's address space and allowed to work inside it, nothing more. In those modules a safety check must not carry a message. The message is not free — it becomes a block of data planted at a fixed address the moment the module loads, and that address lands *inside* the memory it was only borrowing. The check meant to catch corruption becomes the corruption. A lodger nailing shelves to a wall they were told not to touch.

I was midway through a campaign to give every function in the codebase a stated contract. Eight agents ran the sweep in parallel, one per game. The result:

  • Four modules broke it outright — the exact forbidden shape, in the exact modules the rule names.
  • Two more broke it and passed — the optimizer happened to notice the message was never read and deleted it. Correct-by-luck is indistinguishable from correct, right up until the day it isn't.
  • Nothing raised a hand — every check in the loop people actually run stayed green, for days, with the broken artifacts committed.

What found it reads the compiled artifact instead of the source. It does not ask what the code says — it asks what the linker emitted. Is there a block of data in here. Is there a startup routine. Does this touch a stack it was told to leave alone. That check had existed for months. It simply is not one anyone runs by reflex, because it is the slow one at the end of the full pass.

So the rule was written, the writing was read, and the code still came out wrong. That is not a failure of writing. A rule that lives only in prose is a suggestion with good intentions. The only thing that behaved like a rule that afternoon was the check that could say no.

the build, refusing
build: failed — warnings: 1 function without an entry assertion
                          1 new player-readable string literal
                          1 unformatted file

// nothing shipped. the build serial did not move.
// the agents waiting on it are still waiting.

Why the codebase out-votes the prompt

Here is the part that took me longest to accept.

When a language model writes a function, it is not consulting the style guide. It is pattern-matching against what is in front of it. If the surrounding two hundred functions open by checking their arguments, the next one will too — not because a rule said so, but because that is what a function looks like *here*. If three do it and a hundred and ninety-seven don't, the rule loses. Every time.

So the standard is not the document. The standard is the weight of code already written that way.

A specification is a whisper at the front of the context window. Ten thousand lines of consistent practice is a shout that never stops. That inverts the usual instinct: you do not get compliance by writing a better prompt. You get it by making the codebase itself the argument, then defending that majority with something mechanical.

Which is why the gates matter more than the guide. Not because agents are careless — they are extremely agreeable, which is the actual problem. Ask one whether it followed the standard and it will say yes, and mean it. A build that goes red does not negotiate.

Assertions are the specification

This is the rule that changed how the code reads, and the one I would keep if I could keep only one.

Every function asserts in three positions.

  • On entry — each argument's contract, and the relationships between them — this index is below that count, these two lengths are equal, this range is not inverted.
  • Before returning — the result's own contract. This is the half that gets skipped, and it is the half that finds things.
  • After calling — whatever you are relying on from what you just called. Trust nothing across a boundary, including your own code on the other side of it.

Both sides check. A broken contract is then caught at the function that broke it, not three layers downstream where the symptom finally surfaces as a wrong number on a screen.

CALLER◆ checks what it sends◆ checks what came backCALLEE◆ checks what it got◆ checks what it returnsthe boundary
Both sides of every boundary check — whichever one is wrong, the crash lands on it.
the same function, before and after
// before — it takes a number and trusts it
fn spawn_count(base: f32) -> u32 {
    (base as i32 + difficulty_offset()).max(1) as u32
}

// after — the contract, stated at both ends
fn spawn_count(base: f32) -> u32 {
    assert!(base.is_finite(), "spawn count: a base that is not a number");
    assert!(base >= 0.0, "spawn count: a base below zero");
    let result = (base.max(1.0) as i32 + difficulty_offset()).max(1) as u32;
    assert!(result >= 1, "spawn count: below its floor of one");
    result
}

Two details are load-bearing.

The checks are live in the *shipped* build, not only in development. A check switched off in the version players run is not a check, it is a comment.

And the failure is loud. The game stops, names what broke and where, and reloads. Crash early, crash loudly, fix the cause.

WITH THE CHECKbad valuethe checkCRASHnames the line. fixed now.WITHOUT ITbad valuerenderssavesships3 weeks latercannot reproduce
Two lives of the same bad number. The crash is the cheap one.

A game that limps on with a corrupted number does not save you the crash. It defers it, strips the evidence, and hands it to someone else.

The message matters too, more than I expected. It has a fixed shape — where it broke, then the invariant in the present tense. Not *bad count* but *a garrison count that is not a whole number at or above zero*. The first is a shrug. The second is the specification, sitting at the exact line that enforces it, in a form a stranger can read.

◆

The campaign took assertion density from roughly one check per ten functions, with six percent of functions validating their inputs at all, to over two per function and near-total entry coverage across the finished games. The number I care about is the second one, though — density can be faked by padding trivial functions. Coverage cannot.

Memory you allocate once

The other rules are about the machine rather than the reader. TigerStyle states them as a short list, and every one survives the move from a database to a game engine intact.

  • Do things in the most direct way possible.
  • Don't copy memory in the data plane.
  • Don't thrash the CPU cache.
  • Don't serialize or deserialize.
  • Use fixed-size, cache-line-aligned structures.

In practice: entity state lives in flat parallel arrays of fixed-size values, aligned so no two hot fields share a cache line and no structure straddles one. After startup, nothing allocates — no new objects, no temporary arrays, no per-frame closures in any path that runs every frame. Scratch space is claimed once at boot and reused forever.

That is not micro-optimization, it is the budget. Spread a single frame across a very large number of entities and you have a handful of nanoseconds each. One cache miss costs a hundred. You do not get to miss on the way to an entity, and you certainly do not get to copy it across a thread boundary and back — which is why the simulation and the renderer share one block of memory and neither ever hands the other a copy. I wrote that architecture up in its own post; the short version is that the fastest data plane is the one where nothing moves.

A checker enforces the allocation rule by reading the code and failing the build on anything that allocates in a hot path. One-time setup declares itself with a marker comment. And that marker has a trap I walked into this week: it only applies to the function directly beneath it, so slipping a constant in between silently un-exempts the code below. Mechanical rules have mechanical edges.

Names, and a build that goes red

Naming is the cheapest standard to state and the most valuable to hold. It is what makes a codebase feel like one author rather than forty.

  • One casing, both languages — the systems language and the scripting language name things the same way, so moving logic between them is a move, not a rewrite.
  • Big-endian qualifiers — the significant word first, the qualifier appended — *size minimum*, *connection delay milliseconds maximum*. Never the other way around.
  • Related names, same length — so they line up in a column and a wrong one shows up as a ragged edge before you have read a single word.
  • No abbreviations — beyond loop indices and the axis letters. A name is read a hundred times more often than it is typed.

The zero-warnings rule sits beside it and is the one people push back on hardest. Any compiler warning, any lint warning, any file the formatter would rewrite: the build fails. Not a report, not a summary at the end — red, and the version number does not advance, so every agent waiting on that build is blocked until it is clean.

The argument for this is entirely about agents. A warning an agent is permitted to ignore is a warning it *will* ignore, every time, forever, while cheerfully reporting success. There is no middle setting where warnings accumulate but are taken seriously. Either zero is enforced or the count grows without bound.

A ratchet, not a rule

The obvious objection to all of this is that you cannot retrofit a standard onto a large existing codebase. You cannot — not in one pass, and every attempt to try produces a branch nobody can review.

So each gate is a ratchet. The existing violations are counted once and frozen into a baseline file. New violations fail the build; old ones sit in the ledger, visible and shameful and not blocking anyone. Pay some down and the tool re-freezes at the lower number — and refuses to re-freeze if the total went up. Debt can only move one direction.

DEBTTIMEpaid down: allowedadded: build red
The ratchet: existing debt is frozen, not forgiven — it can only go down.

Five of them run on every build.

  • Warnings — compiler and linter, both languages, whole repository, zero tolerated.
  • Formatting — any file either formatter would rewrite. The fix is always the formatter, never a hand edit.
  • Allocation — anything that allocates in a path that runs every frame.
  • Assertions — a new function that takes arguments and checks none of them.
  • Text — a new word a player can read, typed into the source instead of coming from a translation file.

That last one was the biggest surprise. It took player-visible text literals from more than two and a half thousand down to under five hundred, and on the way it found an entire game's worth of character dialogue that had been quietly living in the wrong layer for months. Nobody had noticed, because it looked fine on screen. It looked fine on screen in exactly one language.

What it cost me

I would be selling something if I stopped there. The assertion campaign shipped four crashes into a playable build, and every one had the same shape: a check written against what the author believed rather than what the code actually guarantees.

  • Health is never negative — except a lethal hit legitimately drives it below zero until the death pass floors it later in the same tick. The assertion was right about the invariant and wrong about when it holds.
  • A frame's time delta is always positive — except on the first frame after a resume, and whenever two callbacks land in the same millisecond. Zero is a real frame.
  • This value equals the constant I just wrote into it — except the storage is single-precision and the constant was not, so the value read back never equalled it. That one crashed on every single boot.
  • Memory starts in a valid state — except shared memory starts as zeroes, and zero was outside every range the new checks demanded. The invariants were only ever established when a human pressed Play.

Every one of those was found by *playing the game*, not by review. Which is its own lesson: a standard enforced only by reading is a standard that ships crashes. The gates catch shape; running the thing catches meaning.

And the checks are not free in the other direction either. An assertion that cannot fail is worse than no assertion — it costs something real in the constrained modules, and it teaches every future reader to skim past a line that never mattered. Several of the ones added during the sweep were tautologies, checking a condition the line above had already guaranteed. Those got deleted, not celebrated.

The honest summary: this bought me a codebase where a stranger can predict what the next function looks like, and it cost me a week of chasing crashes I introduced myself.

The standard is the interface

What I actually built here is not a style guide. It is a protocol.

Code moves between different agent tools and different models — the one that drafts a feature is often not the one that reviews it, and neither is the one that fixes it three weeks later. None of them share a memory. The only thing they share is the artifact. A codebase with one voice is a codebase any of them can pick up without a briefing, because the conventions are legible from the code itself rather than from a conversation nobody was part of.

And it is the same property a human needs to stay in the loop. I cannot ask someone to review a thousand lines a day by reading every one. I can ask them to review the *exceptions* — the places where the build went red and someone argued their way past it. That is a review surface a person can actually hold, and it exists only because everything else is uniform enough to skip.

For players, none of this is visible, and that is the point. What they get is a game that runs at the refresh rate of their screen and stops loudly when something is wrong instead of quietly showing them a broken number. The standard is upstream of all of it.

◆

These are small, real games, built in the open as a proving ground for AI-native tooling. Every rule here earned its place by failing first — the gates exist because I broke something, not because I predicted I would.