Engineering
Engineering

Ensuring AI-Generated Code Meets Your Standards

Matthew Watson · 2026.8.4 · 8 min read

If you spend any time reading about AI-written software, you'll quickly find that code quality is a major concern among developers. Opinions run the gamut, from the reactionary all-ai-code-is-terrible to the let-ai-write-whatever-it-wants camp. Feelings aside, ensuring your AI code is up to your standards is a challenge all modern software teams need to solve.

Introzy is an AI-heavy shop. We use Cursor and Claude Code, and we've embraced AI as a tool to improve velocity. With that increased output, standards and workflow become even more important: if you're pushing out sub-par code, you're accumulating tech debt at an alarming rate. Increased velocity is worthless if you have to do everything twice.

We manage this with Markdown commands: plain Markdown files in .cursor/commands/, versioned with the code, that tell the agent how we work, what to skip, and where a human must step in. Four commands cover the lifecycle of a ticket: start new work, pre-PR checks, post-PR automation, and cleanup. This post shows what's actually in them, and two production near-misses that shaped their current form.

The gate that blocks code before it exists

Every ticket begins with one prompt: "Start work on STS-XXX." The most important rule in the workflow fires immediately, and it's not about code style. It's that no code gets written at all.

The command file splits every ticket into two phases, and the phase boundary is a hard stop. Verbatim from start-new-work.md:

PHASE 1: Research & Clarification (I will STOP and wait for your approval) ⚠️ DO NOT PROCEED TO PHASE 2 WITHOUT USER SAYING: "Approved, start implementation"

In phase one, the agent verifies the Linear ticket, moves it to In Progress, pulls the exact branch name from Linear so git and the ticket stay linked, and writes a research document to instructions/research-notes/ with required sections: problem statement, current state analysis, proposed solution, implementation plan, testing strategy, and risk assessment. It searches the codebase for existing patterns to follow instead of reinvent, and asks clarifying questions about scope, edge cases, and testing.

Then it stops. Branch creation is literally step 10 in the file, gated behind the approval checkpoint, so the agent cannot start coding early even if it wants to.

This exists because AI is fast at writing code and bad at knowing what code should exist. The research checkpoint catches wrong assumptions before they become a four-hundred-line PR you have to redo. For trivial fixes we skip the heavy research; for anything touching the database, auth, integrations, or multiple services, we do the deep dive.

Pre-PR: the actual two-pass pipeline

Once a feature is done and manually tested, the pre-PR command takes over. Its centerpiece is a two-pass verification, and here is the actual command chain from the file:

# PASS 1: auto-fix everything (formatters run LAST)
bun run typecheck && bun run lint && bun test && \
  doppler run -- bun run build && \
  bun run format && bun run format:prisma

# PASS 2: verify-only, exactly like CI
bun run typecheck && bun run format:check && bun run format:prisma:check && \
  bun run lint && bun test && doppler run -- bun run build

The ordering is the point. In pass one, the formatters run after everything else, because lint auto-fixes and builds can generate or modify code that isn't formatted yet. Pass two re-runs every check in verify-only mode to prove the formatting didn't break types or tests.

We added the second pass after too many cycles of: fix formatting, push, watch CI fail on typecheck. Whack-a-mole with robots is still whack-a-mole.

If the PR touches Prisma migrations, the command adds two more gates: a timestamp-ordering check (migration folders are named YYYYMMDDHHmmss_description, and a new migration's timestamp must sort after every migration already on main, or deploys apply them out of order) and a schema-drift check, which exists because of a real incident.

What the gates have actually caught

Process posts are easy to write and easy to fake. So here are two specific failures, with the specific fixes now encoded in the command files.

The migration that drifted from the schema

A PR shipped a Prisma migration that did not match schema.prisma. The migration defined deal status enum values as DRAFT, OPEN, CLOSED_WON, CLOSED_LOST while the schema expected PENDING_REVIEW, ACTIVE, COMPLETED, CANCELLED. Worse, the migration was missing an entire table, DealPeriodAmount: 72 lines of SQL that existed in the schema but would never be created in production.

Typecheck passed, because the generated Prisma client comes from the schema, not the migrations. Tests passed, because local databases had been pushed from the schema directly. The drift only surfaced at deploy time, and it took a follow-up PR to untangle.

The fix wasn't "review migrations harder." It was making drift mechanically impossible to miss. The pre-PR command now diffs the migrations directory against the schema using a disposable shadow database:

npx prisma migrate diff \
  --from-migrations ./prisma/migrations \
  --to-schema-datamodel ./prisma/schema.prisma \
  --shadow-database-url "$SHADOW_DATABASE_URL" \
  --exit-code

Exit code 2 means drift, and the agent is instructed to treat it as a hard blocker. The same class of bug cannot reach a PR anymore.

The test mock that leaked across files

Bun runs many test files in a single process, and mock.module() registers a module replacement for the rest of that process, not just the current file. One of our test files mocked the auth module with only the export it needed: { requireAuth: mockFn }. That partial mock replaced the entire visible export surface of @/lib/auth for every test file that imported it afterward.

The symptom was maddening: unrelated tests failing with SyntaxError: Export named 'getAuthenticatedUser' not found, but only in full-suite runs, and only depending on which file happened to execute first. Run the file alone and everything passed.

The rule that came out of it is now in the pre-PR command file, with the exact pattern to use:

import * as authModule from '@/lib/auth'

mock.module('@/lib/auth', () => ({
  ...authModule,
  requireAuth: mockRequireAuth,
}))

Spread the real module, override only what the test needs. For the database package there's a shared createDatabaseMock() helper whose defaults merge with overrides, so mocking one query can't silently delete the rest of the module. And the command requires running the full bun test suite before push, never just the file you edited, because ordering-dependent failures only show up in full runs.

This is the real value of encoding standards in files the agent reads: the painful lesson gets learned once, written down once, and enforced on every ticket after, whether the code is written by a human or an agent.

Post-PR: automating the feedback loop

Creating the PR is where local work ends and the feedback loop begins. The post-PR command handles the tedious parts: open the PR, poll check status, pull deploy logs when preview builds fail, read bot reviews, and iterate.

When our review bots leave comments, the agent triages them. Straightforward findings get fixed without asking: wrong property names, type errors, console statements in server code (banned by lint in favor of our structured logger), exposed secrets. Subjective items get flagged for a human: architecture changes, big refactors, performance trade-offs. High-severity findings block merge until addressed.

The pattern throughout is the same as the research gate: automation speeds up the parts you don't need to watch. It doesn't replace the parts you do.

The loop

Plan, build, verify, ship, reset. The Markdown commands encode our standards once and let the agent execute the repeatable parts. Humans stay at the decision points that matter: approving the plan before code exists, reviewing what ships, and making the calls automation shouldn't make alone.

Two production near-misses are permanently encoded as mechanical gates. That number should keep going up, and that's the point: a standards document that doesn't grow from real failures is just a style guide.

We ship in public. Come build with us.

Introzy is free to start. No card required.