How to Actually Ship AI to Production (And Not Regret It)

12 min read Original article ↗

Mario Villacorta de Prada García

Human-Led TDD: A practical methodology for integrating agentic AI into production development without losing control of your codebase

Press enter or click to view image in full size

It’s been a while since AI got into our professional lives as software developers. Our sector is probably one of the sectors where AI has found the most traction, and for the longest time. The first Copilot now feels like a distant memory compared to the modern coding autonomous agents.

Nevertheless, despite all the industry hype about these tools, if you are a professional developer with some experience, you may have noticed a hard pill to swallow: it’s a gamble to fully trust an agent to ship real production code. Of course, I’m not talking about shipping some light features well-reviewed in an already existing codebase. What I’m referring to is basing a whole product on a semi-blind trust in agentic system output.

We’ve all fallen into a trap. If a system succeeds, it is thanks to AI. If a system fails, it is our fault as developers for not reviewing AI work properly. Heads we lose, tails AI wins.

This feeling, which I believe to be shared among most of us, is not so common among non-programmers. Managers find systems like Claude Code or Gemini CLI to be the Holy Grail of productivity. They may have a point, but a system development doesn’t have only to be fast: it has to be sustainable. This is where the agentic development approach fails miserably.

This may or may not change in the future — I personally believe this to be epistemologically impossible — but what is clear now is that, if our goal is to produce a long-term useful and, eventually, profitable system, we must find a work methodology where we set the pace for agents, and not the opposite. Velocity has never been the aim of software design.

As engineers, not only programmers, we must embrace the past 60 years of this discipline to adapt our development frameworks to integrate agentic interaction all over the coding process. Here’s my take, and a working procedure to fully integrate AI in shipping production code — and not regret it.

The right and duty of code comprehension

Many AI gurus tell us that knowing how to code is counterproductive in the modern AI era, while other non-programmers tell us not to read a single line of code but to test the product and feed back to AI in an allegedly infinite refinement loop. All of this is simply false. Reading and comprehending code is not only a right but a duty, and here is why.

The right

A continuous AI refinement consumes tokens. Every line of code occupies storage. Every executed instruction consumes energy. Unreviewed code represents not only future maintenance costs, but present ones too. As engineers we have the right to optimize them to make our systems more efficient, affordable and profitable.

The duty

The objective of the code we produce is not only to fulfill our desire to build things, but to solve a real problem for a customer. A customer trusts our systems not only based on our intentions, but based on its actual behavior and our accountability for it. If we ship a product that we don’t know how it works precisely, we are selling a lie, which is profoundly immoral — an idea I first came across from @atmoio on X, and I haven’t been able to shake it since.

The AI integrated development methodology: Human-Led TDD

As you may have already guessed, letting AI work and then reviewing its code is not the right approach to truly understand what we’re building. Studies show that code comprehension consistently recruits the brain’s multiple demand system — the same network involved in logic, working memory, and executive control — placing it among the most cognitively demanding tasks a knowledge worker performs (Ivanova et al., 2020).

Luckily, we are standing on the shoulders of giants. We don’t have to reinvent the wheel to produce a brand new approach from scratch, but to rescue what we know that works well. And TDD works fantastically.

Test-Driven Development (TDD) is a coding methodology developed by Kent Beck, the creator of Extreme Programming and one of the signatories of the Agile Manifesto. The idea is simple: the first artifacts to be produced in a codebase are the tests. This is not (only) to ensure that the code is right, but to define and lock in the expected behavior of the system before a single line of implementation is written (Feathers, 2004).

TDD works on a three-step loop, known as Red-Green-Refactor cycle:

Classic TDD cycle
  1. Red: write a small test that defines some expected behavior and execute it. It will fail, since no implementation exists yet.
  2. Green: once the test is written, make it pass in the simplest way possible.
  3. Refactor: once the test already passes, refactor the code to remove duplications.

This process is meant to be not only a testing strategy, but a thinking framework to ensure the correctness and comprehension of every piece of code. As tests precede code, every piece of code is predictable and characterized a priori.

My proposal is to enhance TDD while working with agentic models. Instead of writing test-code pairs one by one, we as developers have to write every piece of test first (Red) and then let the agents make it pass (Green) and Refactor.

Human-Led TDD cycle

Human-Led TDD works then on a four-step loop, the Red-Green-Refactor-Review cycle:

  1. Red: write every test needed to define the expectations of a code. The code defining tests are unit tests, where the test scope should be limited to the code artifact being programmed — the dependencies should be mocked. This kind of test is known as a characterization test.
  2. Green: once the tests are written, hand off to the agent to make them pass.
  3. Refactor: review the code produced, hand off to the agent to refactor if needed.
  4. Review: review the final code produced. Make sure to understand everything and then move on to the next feature.

You define behavior. The agent implements. You verify understanding.

Human-Led TDD and AI-DLC 2026, the perfect match

Some other procedures have also been proposed. One of the most solid and precise ones is AI-DLC 2026, a comprehensive methodology for autonomous agent development. Among its core principles, it establishes that the Autonomous Bolt — the mode where agents operate with minimal human intervention — should iterate against programmatically verifiable Completion Criteria. Tests, type checks, linting, and security scans are the quality gates that reject bad work without prescribing how to do it.

What AI-DLC leaves open is a critical question: who writes those tests? In its default autonomous mode, the agent writes them. Human-Led TDD answers differently: the developer writes the tests, the agent writes the implementation.

This single inversion changes everything. Writing a test is not a mechanical task, but an act of design. It forces you to think about interfaces, edge cases, and expected behavior before a single line of implementation exists. It is, in essence, the most cognitively dense moment of the development process. And it is precisely the moment that should remain human.

Human-Led TDD in practice: a step-by-step example

Red: write characterization tests

Before opening your agent, open your test file. Your goal is to write every test needed to fully characterize the behavior of the artifact you are about to build. These are unit tests — scope is limited to the artifact itself, and all dependencies must be mocked. A characterization test does not test implementation details. It tests the contract: given this input, I expect this output. Nothing more.

For a user authentication service, that looks like this:

import { AuthService } from '../src/auth.service';
import { UserRepository } from '../src/user.repository';
import { TokenService } from '../src/token.service';

describe('AuthService.login', () => {
let authService: AuthService;
let userRepository: jest.Mocked<UserRepository>;
let tokenService: jest.Mocked<TokenService>;
beforeEach(() => {
userRepository = {
findByEmail: jest.fn(),
} as jest.Mocked<UserRepository>;
tokenService = {
sign: jest.fn(),
} as jest.Mocked<TokenService>;
authService = new AuthService(userRepository, tokenService);
});

it('returns a JWT token when credentials are valid', async () => {
// Arrange
userRepository.findByEmail.mockResolvedValue({
id: '123',
email: 'user@example.com',
passwordHash: '$2b$10$hashedpassword',
});
tokenService.sign.mockReturnValue('jwt.token.here');

// Act
const result = await authService.login('user@example.com', 'correct-password');

// Assert
expect(result.token).toBe('jwt.token.here');
expect(result.error).toBeUndefined();
});

it('returns an error when the password is wrong', async () => {
// Arrange
userRepository.findByEmail.mockResolvedValue({
id: '123',
email: 'user@example.com',
passwordHash: '$2b$10$hashedpassword',
});

// Act
const result = await authService.login('user@example.com', 'wrong-password');

// Assert
expect(result.token).toBeUndefined();
expect(result.error).toBe('INVALID_CREDENTIALS');
});

it('returns an error when the user does not exist', async () => {
// Arrange
userRepository.findByEmail.mockResolvedValue(null);

// Act
const result = await authService.login('unknown@example.com', 'any-password');

// Assert
expect(result.token).toBeUndefined();
expect(result.error).toBe('USER_NOT_FOUND');
});

it('does not call tokenService if authentication fails', async () => {
// Arrange
userRepository.findByEmail.mockResolvedValue(null);

// Act
await authService.login('unknown@example.com', 'any-password');

// Assert
expect(tokenService.sign).not.toHaveBeenCalled();
});
});

This takes fifteen to twenty minutes. Every minute is design time — you are defining the contract of your system before a single line of implementation exists.

Green: hand off to the agent

Once the tests are written and failing, give the agent a precise instruction. Vague prompts produce vague code. Your tests are the specification — reference them explicitly:

Implement AuthService in src/auth.service.tsAll tests in tests/auth.service.spec.ts must pass.
TypeScript must compile with strict mode, zero errors.
ESLint must pass with zero warnings.
Do not modify the test file under any circumstance.
The service receives UserRepository and TokenService via constructor injection.
Use bcrypt to compare passwords.
Do not implement any logic not covered by the tests.
When all tests pass, output COMPLETE.
If blocked after 10 attempts, output BLOCKED and document the blocker.

This is the Autonomous Bolt of AI-DLC 2026 in action. The agent iterates against your Completion Criteria until it converges. You do not need to watch, go work on the next test file.

Refactor: review and improve

Once the agent outputs COMPLETE, run the tests yourself to verify. Then read the implementation. Not to rewrite it, but to assess its quality. Ask yourself: is this code I would be comfortable maintaining in six months?

If the implementation passes the tests but feels wrong — too complex, poorly named, missing abstractions — ask the agent to refactor against specific criteria:

The tests pass but the implementation needs refactoring.
Specific issues:
1. The password comparison logic is inline in the login method — extract it to a private method comparePasswords(plain, hash).
2. Error codes are hardcoded strings — define them as an enum AuthError.
3. The method is 40 lines long — it should be under 20.
All tests must still pass after refactoring.
Do not change the public interface.

The key discipline here: you are not asking the agent to rewrite everything. You are giving it specific, targeted instructions. This keeps the refactor bounded and the tests green.

Review: read and understand

This is the step most developers skip under deadline pressure. Do not skip it. Read the final implementation line by line. For each block of code, ask:

  • Do I understand what this does?
  • Do I understand why it does it this way?
  • If this breaks at 3am, could I debug it?

If the answer to any of these is no, add a test that captures that behavior and hand it back to the agent. The review step is not optional: it is the entire point of Human-Led TDD. This is where you convert agent output into knowledge you own.

Each question becomes a new test. Each new test tightens the characterization. Over time, your test suite becomes a precise, executable specification of your system — one that no agent can break without you knowing.

The limits of Human-Led TDD

Vibe coding is fast, there is no denying it. Describing a feature in natural language and watching an agent scaffold an entire module in minutes is genuinely impressive. For prototypes, internal tools, or throwaway scripts, it is probably the right approach. And in some production cases, it might be a real option if the aim of the product is the immediate earnings without future maintenance concerns.

Despite this, vibe coding in production has a compounding problem: every unreviewed decision made by the agent becomes a constraint on the next decision. Technical debt accumulates not in weeks, but in minutes. By the time you realize the architecture is wrong, the agent has already built three more features on top of it. This is where Human-Led TDD shines, it empowers developers to face the challenge of maintaining code they did not write themselves.

Human-Led TDD, however, is not a silver bullet. It requires developers who can write good tests — and writing good tests is a skill that takes years to develop. It is slower than vibe coding in the short term. It requires discipline that is easy to abandon under deadline pressure. What it does guarantee is this: every piece of code that ships to production is code that you, as an engineer, have thought about. Not just tested — thought about.

Conclusion

The AI era does not make engineers obsolete, it makes engineering judgment more valuable than ever. The developer who understands what their system does, why it does it, and what happens when it fails is not a relic of a pre-AI world. They are exactly what production systems need.

Human-Led TDD is not a rejection of AI. It is a framework for using it responsibly. You bring the design thinking. The agent brings the implementation speed. Together, they produce something neither could alone: code that is fast to write and possible to trust.

The industry will keep producing faster agents, better models, and more impressive demos. That is fine. But the question was never how fast we can ship — it was always whether what we ship is worth keeping.

Velocity has never been the aim of software engineering. Correctness has. And correctness, in the end, is a human responsibility.