Building a Repo That AI Agents (and Non-Engineers) Can Safely Ship To
The structural choices — and the mistakes that shaped them — behind a production codebase where most changes are driven by non-engineers through an AI coding assistant.
Give an AI agent commit access to a production codebase and the first thing it will do, sooner or later, is something reasonable-looking that you never asked for. Not because it's careless — because it has no idea what it doesn't know. It will make a one-line schema change without realizing that six other features quietly depend on the old shape. It will invent a clean new pattern instead of noticing the codebase already had one. It will route around a restriction it can't get past rather than stop and ask. None of that is a model-quality problem. Every demo has already answered "can AI write code." The much harder question is whether you can hand real commit access to something with no institutional memory, no fear of breaking things, and no default instinct to ask before it acts — and have the result hold up to the same bar as your best engineer's work, every time.
Over the last few months I've been building exactly that kind of repo — a production SaaS codebase where most day-to-day changes are driven by non-engineers working through an AI coding assistant, with a much smaller number of changes made directly by an engineer. This post is about the structural choices that made that workable, and the mistakes that shaped them. It isn't about the product itself — just the scaffolding around it.
The core problem: AI agents don't have judgment, they have context
An engineer reviewing a PR brings years of accumulated judgment about what's safe to touch, what's likely to break silently, and when a "small" change is actually a big one. An AI agent brings none of that by default — it has whatever's in its context window for this task, nothing more.
The fix isn't "make the AI smarter." It's making the judgment explicit and durable, so it doesn't have to be re-derived (or missed) every session — and making the bar for a finished change the same regardless of who or what is driving.
Requirements docs as the source of truth, not the code
The single highest-leverage change was requiring a requirements-*.md file for any non-trivial module, living next to the code it describes, and treating it — not the implementation — as authoritative for what a feature is supposed to do. Code answers "how"; the requirements doc answers "what" and "why," including the edge cases someone already thought through once.
This sounds like ordinary documentation hygiene, but the enforcement detail matters: the doc has to be updated in the same change as the behavior, not as a follow-up. A requirements doc describing last month's behavior is worse than no doc, because an agent (or a person) will trust it and be wrong with confidence. "Keep docs in sync or don't bother" turned out to be a much stronger rule than "please keep docs updated."
Protected paths as a hard boundary, not a suggestion
Some parts of a codebase are cheap to get wrong and expensive to fix: schema migrations, row-level-security policies, CI/CD workflow files, branch protection. We carved these out explicitly as a "Section 0" in the project's instruction files and gave agents a standing rule: if the correct fix touches one of these paths and you're not the verified core engineer, don't implement a workaround — file an issue and stop.
That last part — "don't work around the blocker" — came from watching the alternative happen. An agent blocked from touching a protected file will, left to its own devices, sometimes route around the restriction rather than through it: a compatibility shim instead of a schema fix, client-side filtering instead of a policy change. Individually reasonable-looking, collectively a mess to unwind. Naming the anti-pattern explicitly stopped it.
Regression prevention belongs in requirements, not in code review
The instinctive place to catch a regression is at review time, after the change is written. We moved that upstream: before finalizing requirements for any change, the agent has to answer "what else reads or writes this data, and does any existing acceptance criterion elsewhere in the repo now conflict with what I'm about to build?" If yes, the other module's requirements doc gets updated first, before implementation starts.
This is slower per-change than "write the code, see what breaks." It is much faster in aggregate, because it converts silent breakage into an explicit, resolved decision made while the full context is still loaded — instead of a bug report three weeks later that nobody can immediately explain.
Making the bar the same, regardless of who's driving
The hardest problem wasn't getting non-engineer-driven changes to work at all — it was getting them to be indistinguishable, in quality, from an engineer's. That required treating "good code" as a checklist an agent can actually execute, not a taste an agent is assumed to have.
Copy the existing pattern, don't invent a new one
The single biggest source of quality drift was net-new abstractions — a new way to structure a server action, a new error-handling shape, a new naming convention — introduced because it was locally reasonable, not because the codebase needed a second way of doing something it already did once. The standing instruction is to find the nearest existing example of the thing being built and mirror its shape: same result-wrapper type, same revalidation pattern, same component structure, same test layout. A codebase where every feature has its own idiom is unmaintainable no matter who wrote it; a codebase where the fifth loan-detail feature reads like the first is reviewable at a glance.
Tests are part of the change, not a follow-up
No feature or fix is "done" without unit test coverage landing in the same commit — new behavior gets new tests, changed behavior gets updated assertions, and a bug fix ships with a regression test that would have caught it. This is enforced the same way for a one-line fix as for a multi-file feature; there's no "small change" exemption, because small changes are exactly the ones that skip review scrutiny.
Type-checking clean is a hard gate
tsc --noEmit has to pass with zero errors before a change is considered finished — not "mostly passes" or "the new errors are unrelated." This catches the specific failure mode of a shared-type edit quietly breaking a dozen call sites that a human skimming a diff would never think to check.
Verify against real state, not against how the UI looks
A feature that visually renders correctly can still be silently writing the wrong thing to the database. The standard is to confirm behavior against the actual persisted data (a direct query, not just "the page looks right after I clicked save") before calling something verified — this catches an entire class of bug that pure UI inspection misses.
One commit per change, and it has to be a commit you'd defend
Requiring a single, amendable commit per branch — rather than a pile of wip / fix typo / address feedback commits — turned out to be a quality forcing function, not just a hygiene one. Squashing your own thinking into one coherent commit message before it ships forces you to articulate why the change is correct, which surfaces gaps that a stream-of-fixups history lets you avoid noticing.
The instruction files are the shared standard, and they're kept honest
AGENTS.md and CLAUDE.md hold the actual, current rules — not aspirational ones — and get amended the moment a rule stops matching reality, in the same spirit as the requirements docs. An instruction file that's drifted from practice is worse than none, because it's the first thing consulted and the last thing checked.
None of this is exotic — it's the same checklist a good tech lead would informally apply to any PR. The difference is making it explicit and mechanical enough that it applies identically whether the diff came from an engineer typing it directly or from an agent executing a non-engineer's request.
Git workflow: the rules are only real if they're unforgeable
The workflow rule is simple to state: non-engineers go through a PR, the one engineer on the team can push straight to the trunk branch. The obvious failure mode is equally simple: an agent operating on someone else's behalf just... says it's the engineer, and the rule evaporates.
The fix was to stop taking the claim at face value. The exception now requires checking actual identity against the connected git host account before it applies — not a self-reported name in the conversation. A rule that can be satisfied by asserting it isn't a rule, it's a suggestion; tying it to something an agent can check but not fabricate turned it back into a rule.
The other durable git-workflow habit: one commit per feature branch, amended (not appended to) as review feedback comes in, force-pushed with --force-with-lease. This keeps history readable for humans skimming it later and makes "what did review feedback actually change" a single diffable thing instead of an archaeology exercise across five fixup commits.
What actually made non-engineer contributions safe
None of the above works if it's advisory. The pieces that held up were the ones with a mechanical checkpoint attached — an identity check, a required doc update in the same diff, a named anti-pattern to refuse, a test-coverage and type-check gate that can't be waved through. The pieces that didn't hold up as well were the ones phrased as intent ("please be careful with shared data," "try to keep quality high") without something checking that intent actually happened.
The pattern generalizes past this one repo: an AI agent will meet a bar exactly as far as the bar is checkable. Anything softer becomes a preference that erodes under time pressure — for a human contributor or an AI one.
What's next
The guardrails above are load-bearing for the current scale of contribution — a handful of non-engineers, one engineer, a few hours between changes and review feedback. Every one of them is a first version, sized to today's throughput, and each has an obvious next step once more people and more agents are touching the repo at once:
- Requirements docs currently stay honest because the rule says they must, and someone has to remember to check. The next step is not trusting memory for that: a lightweight check that flags a merged change touching a documented module without a corresponding doc edit, so drift gets caught mechanically instead of relying on whoever's writing requirements that day to also remember to grep for the old ones.
- Protected paths is a fixed list today — schema, RLS, CI, branch protection. It'll need to grow as new categories of expensive-to-get-wrong surface appear (shared abstractions, cross-cutting config, anything enough features depend on that a "small" edit isn't small). The harder next step is making the boundary self-updating rather than something a person has to notice needs a new entry after the fact.
- Regression prevention still runs on an agent reasoning from context about "what else reads or writes this data" — reliable, but only as complete as what's loaded into that session. The next iteration is real dependency mapping: something that can answer "what depends on this table/field/component" from the actual codebase, not from an agent's best recollection of it.
- The quality bar — pattern reuse, test coverage, type-checking, real-state verification, single defensible commits — holds up today because the checklist is short enough to run by hand every time. It won't stay short as the surface area grows, so the next step is pushing more of it from "the agent is instructed to check" to "the change literally cannot merge until it's true" — gates enforced by tooling, not by an agent remembering the instruction file correctly.
- Git-workflow identity currently hinges on one hardcoded name. The moment a second engineer joins, that has to become a real allowlist checked the same way — against the git host's identity, never a claim in conversation — rather than a single name quietly baked into the rule.
None of these are solved so much as scheduled. The common thread is the same one from every section above: each of these controls is only as good as its weakest, most manual link, and the next round of work is finding and replacing that link before scale does it for us. That's the next post.
Like what you read and want to get in touch?
We're always happy to talk shop, swap notes, or hear about what you're building.