AI-first engineering without the slop
How I use Claude Code, Cursor and Copilot while every generated line is still reviewed and tested: what I delegate, what I don't, and why.
5 min readai assisted development · code review · testing · ci · engineering practice
I call myself an AI-first engineer, and I hold one rule alongside it: all generated code is reviewed and tested. People sometimes read those as being in tension. I think they're the same sentence: AI tools changed how fast code appears, not who is responsible for it.
Claude Code, Cursor and GitHub Copilot are part of how I build. Here's how I split the work between them and me, and what keeps the output from turning into slop.
What AI-first means in practice
The tools overlap, and I use each for what it's good at. Copilot completes the line or block I'm already writing. Cursor is where I make focused edits with the surrounding files in view. Claude Code takes tasks that span many files or need commands run, like a refactor across a module followed by the test suite.
The common thread: I describe the outcome and the constraints, the tool produces a draft, and I treat that draft as a proposal. Nothing reaches a branch I'd merge until I've read it.
What I delegate
- Code that follows a pattern the codebase already has: another endpoint shaped like its neighbours, a form, a DTO, a typed API client.
- Test cases for behaviour I've specified. I list the edge cases, the tool writes the tests, and I read every assertion.
- Mechanical changes across many files, such as renames, API migrations and tightening types, with a test suite in place to catch mistakes.
- Reading unfamiliar code. "Walk me through how this module handles retries" is a fast first pass, as long as I check the answer against the code.
- First drafts of docs, scripts and one-off tools.
- Prototypes I intend to throw away.
What I don't delegate
- Architecture. Service boundaries, data models, where a workflow ends and an activity begins. A model can argue options with me; it doesn't decide.
- Security boundaries. Authentication, tenancy checks, permissions and secrets handling. A tool can draft the code, but I define the rules and read every line against them.
- Destructive data work. Production migrations, backfills and deletes get written or rewritten by me, and get a dry run.
- What "done" means. Choosing what to test and which edge cases matter is design work.
- Anything I can't explain. If I couldn't defend a line in review, it doesn't merge, regardless of who or what wrote it.
Tests and CI are the guardrail
When generating code is cheap, the cost moves to verification. At Sendler.ai I owned quality: unit, integration and end-to-end tests with Vitest, Playwright and Cypress ran in CI before every release. AI-assisted work goes through the same gate as everything else, and the gate doesn't know or care who wrote the diff.
name: verify
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm test -- --coverage
- run: npx playwright install --with-deps
- run: npm run test:e2eThe weak spot is generated tests. Ask a model for tests and it will write ones that pass, and passing is not the same as testing anything:
// Passes, proves nothing: it asserts the mock it just configured.
it('applies the discount', () => {
const pricing = { applyDiscount: vi.fn().mockReturnValue(80) };
expect(pricing.applyDiscount(100, 'SPRING20')).toBe(80);
});
// Exercises the real function on the edge case that matters.
it('never discounts below zero', () => {
expect(applyDiscount(10, { type: 'fixed', amount: 25 })).toBe(0);
});Snapshot tests generated from whatever the code currently outputs have the same problem. They freeze the current behaviour, bugs included.
Review AI diffs like a junior's PR
As a lead I mentored a team of 5 engineers through design sessions and code reviews, and I review AI output with the posture I'd bring to a capable junior's pull request: assume good intent, verify everything. My checklist:
- Scope. Does the diff do what was asked, and only that? Unrequested refactors hide in large diffs.
- Real APIs. Do the imported functions and options exist in the versions we actually use? Check the types, not the confidence of the explanation.
- Error handling. Look for swallowed errors, broad catches and fallback values that turn a failure into quietly wrong data.
- Duplication. Generated code often re-implements a helper that already exists two folders away.
- Dependencies. A new package needs the same justification it would need from a person.
- Security. Input validated at the boundary, tenancy enforced, nothing sensitive written to logs.
Size matters as much as it does with people. I ask for small, reviewable steps instead of one sweeping change, because a huge diff gets skimmed no matter who wrote it.
Context is part of the work
Output quality follows the context the tool has. A project instruction file for Claude Code or Cursor that states the stack, the conventions, the patterns to copy and the commands that run tests and lint makes generated code look like the codebase rather than a tutorial. Asking the agent to run the tests itself and fix what fails tightens the loop further.
None of that replaces review. It means review starts from something that already compiles and passes, so my attention goes to whether the change is right, not whether it runs.
The rule I work by: AI can write any line, and I'm accountable for every line. Delegate the patterned and the mechanical, keep architecture, security and data decisions for yourself, let CI hold the same bar for every diff, and review generated code as carefully as a new teammate's. Done that way, AI-first isn't about producing more code; it's about spending more of your time on the decisions that actually need you.