Compound Engineering With Codex: Keeping Agents Honest
How I keep AI-assisted engineering from becoming a pile of plausible diffs: committed instructions, bounded specs, tests, reviews, and records left behind.
A one-line instruction can hide three weeks of assumptions, and I have watched an agent turn a harmless-sounding request into clean changes across models, controllers, views and tests: the code compiled and its new tests passed, yet it had silently chosen an ownership rule nobody had decided, which is the dangerous workflow because nothing explodes but the review burden arrives after the output.
Coding agents can write code, but the hard part is leaving the repository easier to understand rather than supplying another answer to an unwritten question; it compounds when a decision, test or constraint from one task makes the next safer to execute and cheaper to review.
The repository has to remember what I will forget
The work starts before a prompt, so I commit operating instructions alongside the code, and in one iPhone repository, AGENTS.md says the data model is built in Swift code, not an .xcdatamodeld file, an unusual choice made for diffability and agent editability; the instruction carries the consequence that schema changes are additive, deployed fields are never renamed or retyped, and nobody introduces a parallel model format because it looks familiar.
Every Core Data save or delete goes through saveSurfacingErrors(), paired with a visible error alert, rather than try? context.save(), because a silent failure is not a preference in an app where someone may log a medical observation, dismiss a sheet, see success and lose the record; the rule and failure mode are available before an agent edits, and another project routes an agent through its instructions: load product, design and architecture documents before changing runtime behaviour, load the safety document for trust, privacy, billing, authentication or user-generated content, and keep backend and mobile code in separate architecture documents; the agent must make the smallest safe change, run relevant verification and record a recurring failure as a short note before closing the task.
A constraint left in a review thread, terminal scrollback or my head is half lost, so telling an agent to read twenty documents creates a lottery about what it ignores; use AGENTS.md as a router for what is unusual here, which documents are authoritative and which commands prove it worked, then keep the rest near its domain, but instructions can be wrong - I have seen a guide point at a boundary moved by an architecture change and the agent follow it correctly - so the document was the bug; committed instructions are production material, which means reviewing them and deleting obsolete rules.
A spec directory is a boundary, not a ceremony
For changes larger than a contained fix, I use spec-driven directories under openspec/, where the folder matters less than giving intended behaviour, affected surfaces and acceptance criteria a place to exist before a diff makes them look inevitable, and a good change answers what will be true when it lands, what must stay true, which files or boundaries are involved, what is excluded and how correctness will be established, which turns "add this feature" into a bounded operation rather than an invitation for an agent to invent product policy.
The education platform shows why: it has educators delivering sequenced course material, volunteers accepting opportunities, administrators moderating a showcase and separate identity paths for educator and anonymous student feedback, so a small request can touch completion state, roles, signed public links, reporting and a questionnaire engine; "Wire up feedback" lets an agent choose whichever responsibility it meets first.
When code differs from a change plan, I can ask whether the plan is stale, implementation missed a condition or the requirement changed; without that record, the diff becomes the only account of intent, and although I do not write a formal spec for every typo, authentication, money, background work, destructive actions, safety constraints and data shape need a written boundary because a reasonable default is most likely wrong; say no to a framework migration while adding a button, redesign or shared abstraction because three similar lines offended a model, since a tidy diff can still be harder to review and roll back.
Tests are the contract the agent cannot charm its way around
Generated code reads confidently, which is one of its least useful properties, so the contract has to be executable; I want a test identifying behaviour before implementation detail becomes evidence: a request test for an authorisation boundary, a model test for an invariant, or a system test when failure lives between a Turbo response and Stimulus controller, where test level follows risk rather than a test-pyramid rule, and on the education platform, unique indexes enforce one active delivery per user and course, one completion per delivery, step and user, one volunteer match per request and volunteer, and one volunteer rating per entry - product rules where a generated validation is insufficient when two requests arrive together, so the database must reject the state, and tests must show the application presents that rejection sensibly; the observable contracts are concrete: a completion is recorded once for a delivery and step, a volunteer rates an entry once, a public feedback link expires, and an unapproved volunteer receives no offer.
The property operations platform made this clearer: its import takes paginated source records, writes snapshots, projects canonical records, handles retries and resumes from a persisted cursor, where the cursor advances only after the root record imports, a failed root does not advance it, and older imported data cannot overwrite a newer webhook value, which are behaviours with tests because the cost is quiet data error, not a broken screen; for the historical migration, the strongest contract was a read-only integrity process that built a per-property manifest and refused to declare the source safe to delete while a backfill remained incomplete; it found cover images and meter photographs without UUIDs that an earlier download path skipped, so the answer was to record the gap, repair the path and verify those image categories again rather than call the migration done because the main import ran.
An agent can help write an importer, but it cannot make source deletion safe by sounding certain; only a contract comparing the right things can do that, and I ask an agent to alter tests when behaviour changes rather than repeat its implementation in another file, because two tests sharing one wrong assumption are one mistake with extra evidence; a passing suite only shows the rules we stated. Tests as contracts for AI agents covers that discipline in more detail.
More output moves the bottleneck into review
There is no honest speed multiple because it depends on the task, code, request clarity and how much output I accept - the bottleneck moves, and while an agent can draft a migration, job, controller and tests in one pass, I still inspect queries, transaction boundaries, indexes, ownership checks, error paths, retries and dependencies; for public behaviour, I think like the user, because more files mean more surface area for missed local rules, while unreviewed generated code is especially dangerous because it has the right shape - familiar framework methods, reasonable names and tidy factoring - then holds policy in the wrong layer, catches an error the product should surface or makes a background job non-idempotent because the happy path was easier.
I review in a different order: reread the request and spec, inspect changed boundaries such as routes, schema, permissions, public APIs, job enqueue points and external writes, read tests for plausible uncovered failure, then inspect local code; a beautiful helper does not compensate for updating the wrong row, and I prefer small agent tasks because a narrow request creates a diff I can hold in my head, while "Finish the feature" creates a branch full of choices whose necessity I cannot judge until I review them all; typing is cheap, but attention is not.
When a review comment repeats, decide where it belongs: put an invariant in a test or database constraint, a routing rule in its architecture note and a common procedure in the agent guide, because repeating it in review means the repository failed to retain something.
Memory has to be specific enough to be useful
Agent-memory directories help, but do not replace code or tests; the education project has durable Rails-agent memory, task documents and archived handoff material, which help recover where an integration lives, which surface owns a workflow and what was tried, but memory is soft - it can be stale, incomplete or describe a plan that never landed - so treat it as a lead, then inspect current code and tests; the hierarchy matters: the running system and tests show present behaviour, a database constraint enforces an invariant, a current design or architecture note explains intent, agent memory records working context and an old prompt is historical evidence; resolve disagreement before creating a diff, and make entries useful: "the public student survey uses a purpose-bound signed token with an expiry, do not replace it with a session requirement," names a boundary and reason, whereas "Everything about surveys" is a document title pretending to be retrieval.
The compound part is a closed loop
Start with the smallest clear intended behaviour, load repository instructions and authoritative domain material, put a non-trivial change in a bounded spec directory, request a narrow implementation, run relevant tests, review product and policy decisions hidden in the diff, and preserve the surprise where the next change finds it, which may be a test, one-paragraph operations note, deletion of a false instruction or an explicit decision not to encode a rule we do not know, because invented certainty is not memory - it is future cleanup; I do not want agents changing anything quickly, but want them to encounter the constraints a careful engineer would, have bad assumptions fail close to the change, and teach the next task something concrete, which is how work stays legible after the chat window is gone.
For the wider question of whether an AI-heavy codebase is worth buying or inheriting, read my technical due diligence checklist; I build and review Rails, mobile and integration work at 600 EUR/day, and my CV is here.