AI Coding Agents: Best Practices for Reliable Software
AI coding agents make implementation faster. The harder question is how to trust the result as the software grows. Clear requirements, useful tests, and readable code give developers a way to keep control.
Article summary
TL;DR
- Define the behavior you need before generating code. An agent can implement a precise requirement, but it should not silently decide the product’s rules.
- Observe a new test fail for the expected reason, then check that the implementation makes it pass. This helps expose tests that prove too little.
- Use coverage to find gaps and different test layers to examine different risks. Full coverage does not establish that the product solves the right problem.
- Keep stable behavior protected during refactoring, and keep the reasons for important rules close to the code.
- Judge productivity across the life of a change: implementation, review, release, and maintenance all count.
01 / THE REAL BOTTLENECK
Writing the code is becoming the easier part.
Imagine asking an AI coding agent to add appointment rescheduling to a calendar application. It builds the form, updates the backend, adds tests, and reports that everything passes. You try moving an appointment. It works.

Our imaginary calendar: Alex moves Review from 13:00 to 14:00. Planning stays at 10:00.
Now move it into a time slot that is already occupied.
Should the application refuse? Offer another slot? Allow an override? If it refuses, does the original appointment stay exactly where it was? A polished implementation can leave all of these questions unanswered. The agent may have made reasonable choices, but reasonable choices are not necessarily your product’s rules.
This is the central challenge of using AI coding agents in production. As implementation becomes easier, more of the work shifts toward defining behavior, checking the result, and keeping the system understandable as it grows. Production reliability depends on that whole process.
I explored this with Shep Alderson, a software developer and site reliability engineer with more than 15 years of experience, including work at GitHub. Our conversation is the starting point for this article: how can we use the extra capacity AI gives us to build software we can trust?
The full discussion is available here:
YouTube video
Playing this video connects to YouTube, which may process device and connection data. Read the provider privacy policy.
Let’s follow that question through the calendar example. We will examine best practices for AI coding agents: making requirements testable, interpreting test results, and keeping architecture and documentation useful as the software grows.
The goal is to make the next change easier to judge. That is a more durable advantage than getting the first version onto the screen quickly.
02 / MAKE INTENT TESTABLE
A useful test starts with a decision about behavior.
For our calendar, let’s choose a clear rule: a person cannot have overlapping appointments. If a move would create an overlap, the application must reject it and preserve the original appointment.
We now have something much more useful than “add rescheduling.” We have an outcome that a developer, an agent, and a reviewer can discuss.
Alex’s Planning appointment occupies 10:00 to 11:00, and Review now starts at 14:00. We try moving the 1-hour Review appointment to 10:30. The expected result is a refusal, with both original appointments unchanged. Notice that there are 2 parts to the requirement. An error message alone would not be enough if the application had already moved or deleted the appointment.

Rejecting the move has 2 observable outcomes: conflict feedback and unchanged saved appointments.
This is where test-driven development, or TDD, earns its place in an AI workflow. We express the intended behavior in a test, run it, make the smallest implementation change that satisfies it, and then improve the design while the test remains green.
Why should an AI coding agent see a test fail first?
Seeing the expected failure helps establish that the test can detect the missing or broken behavior. If an agent writes the code and test together, both can share the same mistaken assumption. A green result then shows that they agree, without showing that they satisfy the requirement.
For our example, the useful failure is that an overlapping move succeeds or changes the original appointment. A broken fixture or an import error would not establish that the test protects the scheduling rule.
How TDD makes an AI coding agent’s assumptions visible.
Shep ran into this distinction when agents interpreted his request for TDD as permission to write tests and implementation together. Making the sequence explicit gave him a better way to judge their work. Cursor’s guidance on TDD recommends the same sequence: confirm that tests fail, then implement without changing those tests.
We can take that idea further. Before implementation, read the proposed test as a statement about the product. Does it check the outcome we agreed on? Could it pass while the appointment is lost? Is the expected result coming from the requirement, or simply from what the current code happens to do?
Once the test fails for the right reason, the agent has a concrete problem to solve. When it passes, we have evidence about that behavior. We can then consider the next boundary, such as whether an appointment ending at 11:00 permits another to start at 11:00.
That smaller loop costs time. It can also prevent a vague requirement from spreading through several files before anyone notices. The value comes from resolving uncertainty while the change is still small enough to understand.
Practise writing and interpreting useful tests with the first free lessons of my Python testing course.
View course03 / READ THE EVIDENCE
A green test suite answers the questions you asked.
Our overlapping-appointment test now passes. That is progress, but it does not mean the entire feature is correct. It means a particular example produced the expected result under the conditions of that test.
This sounds obvious until a coverage report shows 100% and starts to feel like a verdict.
Shep’s challenge to aim for full coverage is useful because AI changes the effort involved in writing tests. A team can revisit code it previously left untested. But generating more assertions also creates more material to review. The number alone cannot tell us whether those assertions protect anything important.
Does 100% test coverage make AI-generated code reliable?
No. Full coverage means the measured statements or branches were exercised. It does not establish that the assertions are correct, that missing requirements were implemented, or that the application behaves correctly under every relevant condition. Use coverage to locate gaps, then examine what the tests actually prove.
Give each test layer a clear job.
In our calendar, a unit test can check the overlap rule directly. It is fast and helps us explore boundaries without starting the whole application.
An integration test can exercise the move through the application and its data store. It can check that a rejected move leaves the original appointment intact. That catches a different kind of defect: the overlap calculation may be correct while the surrounding operation handles failure incorrectly.
A browser test can check that a user receives understandable feedback and still sees the original appointment. It connects the backend result to the experience the product promises.

Each outline marks a test’s scope in this example: the rule, the rule with stored data, or the full browser flow.
These tests overlap in subject, but they protect different things. We do not need to reproduce every input combination at every layer. Put detailed rule checks where they run quickly, and use broader tests where components or user journeys need to work together. My comparison of unit, integration, and end-to-end testing shows these boundaries through a concrete Python example.
Keep an eye on what the test accidentally treats as a requirement. Checking that a particular sample appointment has the exact title “Team meeting” may only preserve a fixture. Checking that a rejected move preserves the appointment’s title, time, and owner protects behavior. My article on guiding AI coding agents to write better pytest tests develops this distinction.
Finally, use the application yourself. A test can confirm that a rescheduling form submits successfully. It cannot decide whether making someone open 3 dialogs to move an appointment is an acceptable experience. You can automate checks once you have identified that problem, but identifying it requires attention to how people use the product.
The test suite becomes more valuable when we understand the limits of each check. It gives us specific reasons for confidence and makes the remaining questions easier to see.
04 / KEEP CHANGE SAFE
Good tests should give the design room to improve.
A few months later, imagine that appointment creation and rescheduling have grown into separate, complicated modules. Both contain versions of the overlap rule. A bug fix updates one version and leaves the other behind.
This is a maintenance problem even if every existing test passes. The design makes a future inconsistency more likely, and it takes more effort to understand where the rule lives.
An agent can help find the duplication and move the shared rule into a suitable place. We still need to define the purpose of that refactor: both operations should use the same scheduling decision while preserving their existing behavior.
The tests we wrote earlier now have a second job: protect behavior while the implementation changes. Reject the conflicting move. Preserve the original appointment. Allow the valid move. These promises should survive a change in file structure.
This is why a test of an internal helper call can be a poor substitute. If a safe refactor changes which helper is called, the test fails even though the product still works. We end up rewriting tests alongside the code and lose a useful point of comparison.
There is a related risk with agents: they can change both sides until the suite passes. During a refactor intended to preserve behavior, I would ask the agent to keep those behavioral tests unchanged. If a test genuinely needs adjustment, its reason should be reviewed separately from the implementation.
Existing software may not have that protection yet. Before reorganizing it, add tests around a stable boundary where the important behavior can be observed. A few checks around creating and moving an appointment may tell us more than hundreds of assertions about private functions.
Those checks are often called characterization tests: they record how the system currently behaves. Deciding what to preserve still requires judgment. If the application already loses an appointment after a rejected move, documenting that result in a test does not make it desirable. Treat the defect as a behavior change with its own expected outcome.
The practical benefit is freedom to improve the code without renegotiating every product rule. An agent can do much of the mechanical work. Clear responsibilities and stable tests make the result reviewable.
05 / GIVE THE AGENT CONTEXT
The right context includes the reason for the rule.
Why would an agent duplicate the overlap check when a suitable implementation already exists?
Sometimes it has only seen the part of the repository related to the immediate request. A locally reasonable solution can be a poor fit for the wider system. The same thing happens to a developer who starts editing before learning how the application is organized.
Before a substantial change, ask the agent to inspect related callers, tests, and existing implementations. For our calendar, that means understanding both appointment creation and rescheduling before proposing another scheduling rule. VS Code’s agent workflow guidance follows the same pattern: start with a bounded task, provide relevant context, and verify the result.
Documentation helps when it carries information the code alone cannot explain. “Checks for overlapping appointments” mostly describes the implementation. “Reject a conflicting move without changing the original appointment” records a promise. If the operation later starts deleting before validating, that description gives a reviewer a reason to investigate.
Keep that local contract near the code that owns it. Keep broader decisions, such as which part of the application owns scheduling, in documentation where someone can understand the system as a whole. Different kinds of knowledge belong at different levels.
We also need a way to preserve the working process between tasks. Repository instructions and agent skills can record recurring expectations: inspect existing code, identify the behavior to preserve, run the relevant checks, and explain unresolved failures. A skill is simply a reusable workflow the agent can load when it needs it.
The useful rules are the ones connected to a real failure pattern. If agents repeatedly duplicate a component, clarify where that responsibility lives and require inspection before adding another implementation. Adding more general advice about writing good code may do little to resolve that specific confusion.
Separate implementation and review can help too. Give the reviewer the requirement and the actual change. Ask whether the rejected move leaves the appointment intact, including failure paths. That is a more concrete review task than asking whether the code looks good.
A separate agent can provide fresh context, but agreement between agents is not independent proof. They may share an assumption. Tests, static checks, and a human reading the change provide other ways to examine the result. Run those checks on every pull request so verification does not depend on the agent remembering to do it.
These practices are also useful when one agent handles the whole task. The key is making the relevant knowledge available and giving each stage a clear purpose. My OpenAI Codex full course shows repository guidance, testing, and review applied across a working project.
06 / THINK BEYOND THE MERGE
Reliability continues after the tests finish.
We have a clearer requirement, useful tests, and a design that is easier to change. There is still a difference between checking a rescheduling operation in isolation and operating the application for real users.
Consider 2 people rescheduling different appointments in Alex’s calendar into the same free slot at almost the same moment. A test that sends requests one after another may never expose that case. Both requests could observe an available slot before either change is stored.

Both requests can pass the availability check before either saves. Sequential tests may never expose this ordering.
We do not need to turn every calendar change into a concurrency project. We do need to recognize when the promised behavior depends on what happens between simultaneous operations. The developer’s job is to identify that risk and require an appropriate safeguard and check. An agent can help implement them once the requirement is understood.
The same reasoning applies to permissions. A move can be valid according to the calendar and still be unauthorized for the person requesting it. A well-tested scheduling rule says nothing about an access check that was never included in the task.
This is why supervision should depend on the consequences of failure. A cosmetic change and a change to who can modify appointments deserve different levels of scrutiny. The important question is what damage a plausible mistake could cause.
Release decisions need another kind of preparation. If rejected moves suddenly increase after deployment, how would the team notice? Is there enough information to investigate? Could the new behavior be disabled or the code rolled back?
A rollback also has limits. Reverting the code does not automatically restore appointments that a faulty version has already deleted. That consequence should affect how we review the change before it reaches users.
Thinking this through connects implementation to the life of the software. Production readiness includes the team’s ability to detect a failure and respond to it. The relevant checks depend on the system; they cannot be replaced by a universal prompt that says “make it production-ready.”
This is also a useful way to experiment with agents. Give them room on work where mistakes are recoverable, observe where they need help, and use that experience to choose how much autonomy to allow on the next task. Trust becomes grounded in what you have checked.
07 / USE THE EXTRA CAPACITY
Spend the productivity gain on software you can maintain.
The most interesting possibility in AI-assisted development is how much more engineering work becomes practical. A small team may be able to cover neglected failure paths, simplify a troublesome module, or improve documentation that used to wait indefinitely.
Those improvements also need judgment. More generated tests can make review harder. More documentation can bury the useful constraint. More code can make the next change slower. The value depends on whether the work reduces a real source of uncertainty or maintenance effort.
That is why fundamentals remain worth learning. Understanding data flow helps you notice when an error response hides a state change. Understanding module boundaries helps you spot duplicated business rules. Knowing how tests fail helps you distinguish protection from reassuring output.
For our calendar, the result we want is concrete: a developer can explain how rescheduling works, point to checks for the important rules, and change the implementation without losing those guarantees. The team can also recognize and respond to a failure after release. That is a useful outcome regardless of who typed the code.
Start with one change you already need to make. State the behavior that matters, identify the question you are least confident about, and use the agent to help answer it. Keep the improvement small enough that you can understand why it worked.
Build this testing judgment through practical Python examples, from useful assertions to automated CI checks.
View courseMore field notes
Keep reading.
Write Python Tests with AI: Build a Pytest Skill
See how AI-generated tests can repeat a bug or expose it. Connect clear requirements to a pytest testing harness, then collect the workflow into a reusable skill.
Python testingpytestOpenAI Codex Tutorial: Free Full Course with Python
A free, 3-hour, project-based OpenAI Codex tutorial for Python developers. Build, test, deploy, and review a real app with Codex.
OpenAI CodexCodex CLIHow to Onboard a Python Team to Testing with Pytest
Learn how a 15-developer Python team used shared practice, legacy-code pinch points, and CI to turn pytest into an everyday engineering habit.
pytestPython testing