For most of this year, almost all of the code I've shipped at work has been written by AI coding agents, Claude Code and Codex. These weren't demos or side projects. First it was a production API platform for scientific data, and now I'm back in identity and access management, where mistakes cost more. I still own every line that merges. The agent types, but I'm accountable.
This isn't a hype piece. It's what has actually worked for me, what hasn't, and the habits I've had to build to stay honest about the difference.
Start from a specification, not a prompt
Agents are very good at writing code that matches something. They're much less reliable at deciding what that something should be. If you hand an agent a vague requirement, it will fill the gaps confidently, and often plausibly, but not always correctly.
So I don't start with a prompt. Requirements usually arrive as documents or spreadsheets, and the first step turns them into a structured specification. Code generation only starts once the spec exists, and the spec becomes the contract between me, the agent and the reviewers. A few things matter more than I expected:
- Ambiguity is a blocker, not a detail. An agent should list open questions, not quietly answer them.
- Specs need versions and an owner. When a requirement changes, I want to diff the old spec against the new one, not re-read the whole document.
- Every requirement should point to a spec section. Anything that doesn't is a gap, and gaps get reported before any code is written.
I package this kind of procedure as a skill, a small, versioned instruction file the agent loads when the task matches. A stripped-down skeleton looks like this:
---
name: requirements-to-spec
description: Use when turning a written requirement into a service
specification, before any code is written.
---
# Requirements to spec
1. Read the source requirement. Quote each statement you rely on.
2. List every ambiguity as an open question. Do not invent answers.
3. Write the spec: operations, request and response shapes,
error cases and non-functional constraints.
4. Map each requirement to the spec section that covers it.
5. Report uncovered requirements and open questions, then stop.
Treat skills like code, including deleting them
Skills beat long, copy-pasted prompts because they're reusable, reviewable and versioned. But they aren't free: every skill an agent can see takes up a little of its context, in every session.
At one point I had far more skills installed than I actually used. Instead of guessing, I went through my session logs, counted which skills had really been used, and removed or hid the rest. A skill is code, and code that's never called is just weight.
Run agents in parallel, but give each one a lane
The biggest change to my day was running several agent sessions at once, each owning one ticket or one pull request. That works well, with two conditions:
- Isolation. Each task gets its own git worktree, so agents never step on each other's checkouts.
- Verified handovers. Sessions can brief each other, which is genuinely useful. But I treat a briefing from another agent the way I'd treat a colleague's notes: a starting point, not a fact. Anything that matters, like a permission, a version or a claim that something is blocked, gets re-checked before anyone builds on it.
Parallelism raised my throughput. It also moved the bottleneck, which turned out to be the most important lesson of all.
Review is where the defects are found
When I looked back at where the real defects in agent-written code were caught, most weren't caught by unit tests. They were caught in review: a separate agent pass over the diff, automated pull-request review, and human reviewers.
The kinds of problems differed too. Review found the issues tests aren't designed for: unsafe handling of untrusted input and credentials, drift from the published API contract, concurrency mistakes, and resilience gaps such as retries that never stop or deadlines that don't actually bound anything. Tests mostly caught logic errors in freshly written code.
So review gets the most structure. My multi-agent review loop looks roughly like this:
- A separate agent session reviews the diff with fresh context, never the session that wrote the code.
- Every finding is verified against the code before it goes anywhere. Review agents and bots produce false positives, and relaying them unchecked wastes everyone's time.
- Findings are triaged as blocking or non-blocking.
- Earlier review threads are read first, so we don't raise duplicates or quietly reverse points already settled.
- Nothing outward-facing is posted unseen. I read the draft of every PR comment and ticket update before it goes out.
The core of the review instruction is short:
Review this diff for correctness, security and contract drift.
For each finding: file, line, what breaks, and a concrete input
that triggers it. Mark it blocking or non-blocking.
Do not report style unless it hides a bug.
If you cannot construct a failing scenario, say so and drop it.
That last line matters most. It turns "this looks risky" into something you can check.
Tests still matter, for a different reason
None of this makes tests pointless. Their role just changes.
- Tests as evidence. When an agent says it has fixed something, I want proof the test can fail. The quickest check is to temporarily reverse the fix and watch the test go red. If it stays green, the test isn't protecting anything.
- Tests as memory. Every defect found in review gets a regression test, so it can't quietly come back.
- The right kind of test. Some of the bugs that were hardest to see were wiring problems that only show up when the real application context starts. Heavily mocked unit tests passed straight over them, while tests that boot the actual context caught them.
There's also a cost to admit: agents write wrong tests too. Debugging an agent's test is real work, and it's easy to overlook when you're counting how many tests were written.
What doesn't work
- Trusting "done". Agents are optimistic. "All tests pass" means nothing until I've seen the output.
- A negative result without a check. I once got a confident "zero" from a search that had silently failed. Now every "none found" comes with a check that the same method finds something I know exists.
- Letting agents act on the outside world unsupervised. Code they write gets reviewed; comments, tickets and messages they would post get the same treatment.
- Very long sessions. Context fades and summaries lose detail. Small, well-defined tasks and notes written to files work better than one session that tries to remember everything.
The habits that keep it honest
If I had to reduce this to a checklist:
- Evidence before claims.
- A check for every negative result.
- One worktree per task.
- A draft before every outward-facing action.
- Verify review findings before relaying them.
- Measure before changing the tooling.
Where this leaves the engineer
My job has moved from typing code to specifying, reviewing and deciding. That doesn't make it easier. Judgement matters more when the code arrives faster than you can read it. The engineers who'll do well with these tools are the ones who are clear about what they want, who check carefully, and who stay accountable for what merges.
I drafted this article with an AI agent and edited it myself.