Before you hire an AI coding agent, fix your contracts
Why the teams that get leverage from AI aren’t the ones with the flashiest model—they’re the ones with enforceable engineering contracts at three layers: code, tests, and human–agent specs.
Originally published on Medium (Dec 2025).

AI doesn’t magically “understand your codebase.” It navigates whatever boundaries you’ve made explicit — and it crashes into the ones you’ve left as vibes.
I’ve been watching teams roll out AI coding tools the way we used to roll out Slack: “Just give people access and it’ll organically improve productivity.”
Sometimes it does.
But more often, the first few weeks look like this:
- The agent confidently edits five files, and nothing compiles.
- It “fixes” a bug by changing the payload shape in a frontend component — without touching the backend.
- It adds a new helper function that duplicates an existing one because it couldn’t tell which one was canonical.
- It writes tests that assert the current behavior (including the bug).
And then the postmortem conclusion is some variation of: AI isn’t ready.
Maybe. But I keep noticing something else:
The teams that get real leverage from AI. Beyond autocomplete, beyond “write me a function”; aren’t the ones with the fanciest model. They’re the ones with the most enforceable contracts.
Not legal contracts. Engineering contracts.
The kind your system can actually check.
I think AI coding succeeds when you have contracts at three layers:
- Code ↔ code: Interfaces are explicit. Modules don’t communicate by wishful thinking.
- Intent ↔ code: Tests encode what “correct” means, and CI enforces it.
- Human ↔ agent: Specs make it hard for an agent to optimize the wrong thing.
If you don’t have these, you can still use AI. You’ll just spend your time supervising it like a very fast intern who never sleeps and sometimes hallucinates.
If you do have them, the agent stops being a novelty and starts being an honest-to-god teammate.
Let me explain what I mean, with the inverse example first.
The “vibes-based interface” problem
Picture a team with a constellation of micro-frontends and microservices.
Everything is JavaScript.
The backend returns JSON.
The frontend code says:
const user = await fetch('/api/user').then(r => r.json())
…and then somewhere else:
if (user.plan === 'pro') {
// ...
}
What is user? Who knows. Whatever the backend felt like returning that day.
There’s no shared schema. No generated SDK. No runtime validation. No consumer-driven contract tests. The “contract” is tribal knowledge + a handful of Slack messages + the memory of someone who left six months ago.
Now add AI.
You ask an agent: “Update the billing page to show the annual price.”
The agent tries to follow the code paths. It sees a user.plan. It sees a user.subscription. It sees three different endpoints that might be relevant.
It makes a guess.
And here’s the catch: in a codebase where the boundaries aren’t explicit, guessing is rational behavior. Humans do too.
The agent didn’t do something uniquely “AI.” It did what humans do in messy systems — except it does it faster, with more confidence, and with a higher tolerance for changing things it doesn’t fully understand.
That’s why the first step to getting value from AI coding is not “better prompts.”
It’s making your system less guessy.
Which brings us to contract layer #1.
Contract #1: Code ↔ code (make boundaries explicit)
When I say “code ↔ code contract,” I mean: if one part of the system depends on another, the dependency should be describable and verifiable.
Examples:
- TypeScript with strict types (not
anyeverywhere) - OpenAPI specs for REST APIs
- Protobuf IDLs for gRPC
- Generated SDKs that clients use instead of hand-rolled fetches
- Shared schema packages (carefully versioned)
- Consumer-driven contract tests (Pact and friends)
Why does this matter specifically for AI?
Because types and schemas are ambiguity killers.
If the contract is explicit, the agent can:
- Update the backend response shape and immediately see compilation errors in the frontend
- Refactor a module and have the type checker scream where it broke downstream consumers
- Navigate the codebase by following real interface definitions rather than naming conventions
A helpful mental model:
Types and schemas aren’t documentation.
They’re machine-readable boundaries.
And AI agents are machines.
The generated SDK trick
One of the easiest “AI-enablement” moves I’ve seen is: stop letting your frontend invent types at the edge.
Instead:
- Backend publishes OpenAPI (or protobuf).
- You generate a client SDK.
- Frontend consumes the SDK types.
Now when the agent touches the billing page, it’s not reverse-engineering JSON payloads from ad-hoc fetch calls.
It’s working with explicit types.
When it’s wrong, it’s wrong in a way the compiler can catch.
This is the difference between “agent as guesser” and “agent as editor.”
But even perfect types don’t fully solve it.
Because you can have beautifully typed code that is… behaving incorrectly.
That’s contract #2.
Contract #2: Intent ↔ code (tests as executable contracts)
There’s a line I keep coming back to:
AI output is only as good as the feedback loop you can give it.
Humans can operate with fuzzy feedback. We have intuition. We can sense that a change “feels risky.” We can read between the lines of a bug report.
Agents can’t.
Agents need:
- A target
- A constraint
- A yes/no signal
Tests are the yes/no signal.
They translate intent (“this should work”) into executable truth (“this passes”).
And CI turns that truth into enforcement.
The red/green loop is the superpower
If you’ve ever watched an agent work with a good test suite, it’s almost eerie.
You ask it to implement a feature.
It writes code.
It runs tests.
It fails.
It changes code.
It runs tests.
It fails in a different way.
It changes code again.
Eventually it finds the path that makes the system go green.
That’s not “understanding” in the philosophical sense.
That’s search with feedback.
And it’s incredibly effective, if your tests encode the right intent.
What tests do you actually need?
I’m not going to do the whole “testing pyramid” sermon here, but I will say:
- Unit tests are the easiest on-ramp (and often the best ROI for agents to write).
- Contract tests (consumer-driven, schema validation) are mandatory once you have multiple services.
- A small set of critical integration/e2e tests gives you reality checks where unit tests lie.
The point isn’t “test everything.”
The point is: give the agent a map of where it’s allowed to walk.
CI is the enforcement layer
A test suite without enforcement is like a speed limit without a cop.
If you want agents to be useful, you need CI gates that are not negotiable:
- Typecheck must pass
- Lint/format must pass
- Tests must pass
- Contract checks must pass
- A few integration/e2e checks must pass (especially around money, auth, permissions)
This is where a lot of “AI rollouts” quietly fail.
They give everyone an agent, but they don’t tighten the pipeline.
So the agent generates plausible diffs that nobody can reliably verify.
Or worse: diffs that “look right” in code review and only break later.
Which leads to the third contract — the one nobody likes to talk about because it sounds like process.
Contract #3: Human ↔ agent (specs that prevent misalignment)
When a human engineer gets a task, they import a ton of implied context:
- What does “done” look like here?
- What style do we use?
- What counts as breaking changes?
- Which corners are acceptable to cut?
- What shouldn’t be changed?
Agents don’t import that context unless you provide it.
So the “human ↔ agent contract” is basically: how do we make expectations explicit enough that an agent can execute without inventing its own definition of success?
I’ve found the most effective format is a lightweight RFC/PRD style document that includes:
- Goal (what outcome we want)
- Non-goals (what we’re explicitly not doing)
- Acceptance criteria (observable behaviors)
- Constraints (performance, security, backward compatibility)
- Definition of done (tests updated, docs updated, feature flagged, etc.)
If you want to go one step further, you can standardize a prompt template that says something like:
- “Do not change public APIs without calling it out.”
- “Add/modify tests that would fail before your change and pass after.”
- “Update docs and changelog if needed.”
- “Prefer the existing patterns and utilities; don’t invent new ones.”
This sounds obvious.
It’s also the difference between:
- an agent that creates impressive-looking code
- …and:
- an agent that ships something your team will actually keep.
Here’s the uncomfortable bottom line:
In messy environments, the agent will often do exactly what you asked — just not what you meant.
Human ↔ agent contracts reduce the gap between “asked” and “meant.”
The context problem: monoliths have an unfair advantage
Even if you nail the three contracts, there’s still one pragmatic constraint: context.
Agents work best when the relevant code is:
- nearby
- searchable
- consistent
- changeable in one atomic diff
This is why monoliths/monorepos (or modular monoliths) have a real advantage.
Not because microservices are “bad.”
Because cross-repo orchestration is still awkward.
In a microservices world, a “simple” feature often requires:
- updating a backend service
- bumping a version
- regenerating a client
- updating a frontend
- coordinating deploy order
- migrating data
A human can orchestrate this. Today, most agent setups can’t do it end-to-end without careful supervision.
So if you live in multi-repo land, the playbook isn’t “give up.” It’s:
- double down on contract testing (consumer-driven contracts become non-negotiable)
- be disciplined about versioning and compatibility
- invest in internal developer platforms that make cross-service changes more repeatable
- standardize schemas and SDK generation so agents have fewer degrees of freedom
In other words: if your architecture increases the surface area of coordination, your contracts have to carry more weight.
A practical maturity ladder (so this doesn’t stay abstract)
If you’re trying to make AI coding work across an engineering org, here’s the sequence I’d actually recommend.
Level 1: Make code boundaries checkable
- Turn on strict typing where possible
- Introduce OpenAPI/protobuf as the source of truth
- Generate SDKs
- Add schema validation and/or contract tests
Level 2: Make intent executable
- Add unit tests for the “hot paths” you keep touching
- Add a small set of integration/e2e tests for the things that can’t break
- Make CI gates required
Level 3: Make agent work legible and aligned
- Adopt lightweight PRDs/RFCs with acceptance criteria
- Create prompt templates that encode team conventions
- Require agents to produce changelogs / test plans / rollout notes (even brief ones)
Only after that would I start worrying about:
- which model is best
- which agent framework is hottest this month
- whether you should have “autonomous PRs”
Because without contracts, autonomy is just a faster way to produce unreviewable changes.
TL;DR: AI doesn’t replace engineering discipline. It rewards it
The biggest misconception I see is treating AI coding as a shortcut around good engineering hygiene.
But the pattern I’m seeing is the opposite:
- If your system is disciplined, AI compounds that discipline.
- If your system is undisciplined, AI compounds the chaos.
AI is a force multiplier.
Contracts determine what it multiplies.