Skip to content

A definition of done for humans and agents

Works on my machine is not a definition of done. A field guide to the checks that make done mean done, for human pull requests and AI agents alike.

“Works on my machine” was always a joke with a hostage in it. It means the code ran once, on one laptop, under conditions nobody wrote down and nobody can reproduce. For twenty years that was a punchline about environments. Now it is a governance problem, because the machine in question might not belong to a person at all.

Agents write production code now, in volume, on your repo. Developers took to it fast: 84% now use or plan to use AI tools, up from 76% a year earlier, in Stack Overflow’s 2025 survey. Then they had to read the output. The single biggest frustration in that same survey, cited by 66% of developers, is “AI solutions that are almost right, but not quite,” and 45% say debugging AI-generated code eats more time than it saves. DORA’s 2025 report tells the same story from the delivery side: AI finally started lifting throughput, a reversal from the year before, and it still made delivery less stable. Its diagnosis reads like a spec for this whole post: “without robust control systems, like strong automated testing, mature version control practices, and fast feedback loops, an increase in change volume leads to instability.”

The bottleneck moved from writing code to verifying it, and a definition of done that lives in a senior engineer’s head does not survive that move. It cannot be applied to fifty parallel agent sessions, and it was never applied consistently to the humans either.

So here is the claim this whole field guide defends: a modern definition of done is an executable contract, not a cultural aspiration. It has to be checkable by a machine, because a machine is now one of the parties doing the work. And the contract that makes an agent’s output trustworthy is the same one that makes a human team’s day enjoyable, for the same reason: nobody has to hold the rules in their head.

Done is a contract, not a feeling

Scrum has had a definition of done for years. The 2020 Scrum Guide calls it “a formal description of the state of the Increment when it meets the quality measures required for the product.” Martin Fowler named the mechanism that makes it real: self-testing code, where “a single command that executes the tests” is enough to tell you whether anything is broken. Both are right. Both were written for a world where every author was a person you could ask to care.

The upgrade for 2026 is small to state and large to implement: the contract has to be executable end to end, by a command, with no human in the loop for the parts a machine can check. Not because people are unreliable, but because people are now outnumbered on volume. When a fair share of your diffs are written by something that does not get embarrassed by a failing build, “the team knows what good looks like” is not a control. A command that returns green or red is.

The working rule is blunt. Done is not a state of mind. It is a set of checks that returned green. If you cannot name the command that proves a change works, the change is not done, and “I ran it locally” is the confession, not the defense.

The four gates

A good definition of done is layered, and the layers are ordered by cost. Each gate catches a different class of failure, cheapest first, so the expensive gates only ever see the problems the cheap ones could not.

A pipeline of four gates a change must pass to be done: Local pre-flight on a laptop running format, lint, typecheck and fast unit tests; the Pull Request gate where a reviewer reads a small diff with tests included; Continuous Integration running the full suite on a clean machine; and Deploy to production behind a flag while the four DORA signals are watched. Each gate is red until its checks pass green.

Gate one, local pre-flight. Format, lint, typecheck, and the fast unit tests, all behind one command, run before you even open a pull request. The point is to fail in ten seconds on your own machine instead of twelve minutes on a shared runner. Wire the real tools behind one script, so nobody runs them by hand and no agent skips a step:

// package.json  (a real Node + TypeScript setup)
{
  "scripts": {
    "format:check": "prettier --check .",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "verify": "npm run format:check && npm run lint && npm run typecheck && npm run test"
  }
}

Now npm run verify is the whole of gate one: the same command a developer runs before pushing, a Git hook can run before a commit, and CI runs on every push. One name for “done,” everywhere. (In a Python shop the same idea is ruff format --check . && ruff check . && mypy . && pytest behind a make verify or a nox session. The tools change, the single entry point does not.)

Gate two, the pull request. A human, or a fresh agent in a clean context, reads the diff. This is where judgment lives: is this the right change, is the design sound, does the code say what it means. It only works if the diff is small enough to actually read, which is a whole section below.

Gate three, CI. The full suite runs on a clean checkout, on every push, on a machine that is nobody’s. This is where “works on my machine” goes to die, because the machine belongs to no one and remembers nothing. Fowler’s original bar still holds: integrate at least daily, and verify every integration with an automated build that includes the tests. If CI is green on a clean checkout, the environment excuse is gone.

Gate four, deploy. The change reaches production behind a feature flag or a canary, and you watch the four things DORA has measured for a decade: deployment frequency, lead time for changes, change failure rate, and time to restore. Elite teams deploy on demand, get a change to production in under a day, keep change failure rate in the low single digits, and recover in under an hour. Those are not vanity metrics. They are the read-out on whether your definition of done is actually holding.

The one command is what makes gate three almost free to stand up. CI runs the same checks npm run verify chains locally, on a machine that belongs to no one. Everything else in the file is just the housekeeping that keeps CI honest: pin the runtime, install from the lockfile, cancel superseded runs, and split the checks into named steps so a red X tells you which gate failed instead of making you read a log.

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:            # every PR; the push trigger covers main itself,
                           # so feature branches don't run CI twice

concurrency:               # a newer push cancels the in-flight run on the
  group: ci-${{ github.ref }}   # same ref, so you don't pay for stale builds
  cancel-in-progress: true

permissions:
  contents: read           # least privilege: CI only needs to read the code

jobs:
  verify:
    runs-on: ubuntu-latest
    timeout-minutes: 15    # a hung test can't burn a whole runner-hour
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm        # cache ~/.npm keyed on package-lock.json

      - name: Install (exact, from the lockfile)
        run: npm ci

      # The same scripts `npm run verify` runs locally, as named steps.
      # `if: ${{ !cancelled() }}` keeps every check running after one fails,
      # so one push surfaces all the failures, not just the first.
      - name: Format
        run: npm run format:check
      - name: Lint
        if: ${{ !cancelled() }}
        run: npm run lint
      - name: Typecheck
        if: ${{ !cancelled() }}
        run: npm run typecheck
      - name: Test
        if: ${{ !cancelled() }}
        run: npm run test -- --coverage

Mark the verify job as a required status check in your branch protection, and a red build physically cannot merge. The gate stops being a habit someone has to remember and becomes a rule the platform enforces, on your pull requests and an agent’s alike.

Notice what threads through all four: tests are part of done, not a follow-up ticket. A feature that ships without a test is a feature that will regress silently the first time a person, or an agent, refactors anything near it. “I will add tests later” is how the suite stops being trustworthy, and an untrustworthy suite fails every gate above at once, because nobody believes the green.

Small pull requests are the load-bearing wall

If you keep one number from this guide, keep this one. Google’s own engineering practices tell their engineers to write changes “smaller than you think,” because reviewers almost never complain that a change is too small, and a small change comes back fast while a big one sits for days. One of the most cited studies on the subject, SmartBear’s review of a Cisco team’s 2,500 code reviews across 3.2 million lines, found that a reviewer’s ability to find defects falls off a cliff past roughly 400 lines of code, and past a pace of about 500 lines per hour. A 200-to-400 line review, done in an hour, surfaces 70 to 90 percent of the defects that are there to find. Beyond that, you are not reviewing, you are scrolling.

This is not a matter of taste. It is the measured limit of human attention, and it is precisely the thing AI breaks. An agent will hand you an 800-line diff without a flicker of hesitation, and that is exactly the uncontrolled change volume DORA warns turns into instability when the testing and feedback loops are not there to catch it. Your definition of done has to refuse the big diff on principle, from a person or a model, because the review that is supposed to catch its bugs cannot physically do so.

Branch and commit hygiene are the cheap habits that keep pull requests small and history readable:

  • Short-lived branches, one purpose each. Cut from an up-to-date main, live for hours or a day, merge, and disappear. A branch that lives for two weeks is a merge conflict and a lost thread waiting to happen.
  • Conventional commits. A one-line grammar, type(scope): summary, with types like feat, fix, refactor, test, and chore. The Conventional Commits spec is worth adopting because it makes history machine-readable: changelogs and semantic version bumps fall out of it automatically, and an agent can generate a correct message every time from a rule it can actually follow.
feat(auth): add Google OAuth callback handler
fix(billing): stop double-charging on retried webhooks
test(auth): cover session-timeout refresh path
refactor(api): extract pagination into a shared helper

The pull request template is where you turn the definition of done from a wiki page nobody reads into a checklist that sits in front of every merge. Drop this in .github/pull_request_template.md and it pre-fills every PR the team opens:

<!-- .github/pull_request_template.md -->

## What & why
<!-- One paragraph: what changed, and the problem it solves. -->
Closes #

## Definition of done
- [ ] `npm run verify` passes locally (format, lint, typecheck, tests)
- [ ] New behavior is covered by tests; the suite is red without this change
- [ ] Diff is under ~400 lines, or split into reviewable commits
- [ ] Branch is short-lived and cut from an up-to-date `main`
- [ ] Docs / `CLAUDE.md` updated if behavior or workflow changed
- [ ] No secrets, keys, or stray debug logging in the diff

## How to verify
<!-- The exact commands a reviewer runs to see it work, and what they should see. -->

That last section matters more than it looks. “How to verify” forces the author, human or agent, to hand the reviewer the receipt instead of a promise. It is the difference between a review and a leap of faith.

Write the contract down where the agent reads it

Here is what most teams are getting wrong right now. They have a definition of done for humans, in a wiki, and they let agents work with none of it. The agent inherits nothing, so it ships to the standard of its training data instead of your team’s. Then someone is surprised when the output needs three rounds of correction that all say the same thing.

The fix is to compile your definition of done for a different reader. For Claude Code that reader is CLAUDE.md, a file the agent loads at the start of every session. Anthropic’s own best-practices guidance is to “keep it concise” and to include the bash commands, testing instructions, code style, and “repository etiquette” the agent cannot infer from the code alone. That is your definition of done, written for the machine that is about to do the work:

Two authors, one contract. A human developer and an AI agent both produce a code diff. The agent is handed the team’s rules through a CLAUDE.md file so it starts from the same standard the human holds. Both diffs flow into a single pull request that must pass the same gates: local verify, the PR review, and CI. The pipeline does not care which author produced the change.

# CLAUDE.md

## Commands
- Install deps: `npm ci`
- Verify everything (run before calling any change done): `npm run verify`
- One test while iterating: `npx vitest run src/path/to/file.test.ts`

## Definition of done (all of these, on every change)
- `npm run verify` passes, and you paste the output. "Looks fine" is not done.
- Every new behavior has a test. If the suite is green without your change,
  the test is missing. Write the failing test first.
- Diff stays under ~400 lines. Split larger work into separate PRs.

## Git workflow
- Never commit to `main`. Branch from an up-to-date `main`, push, open a PR.
- Conventional Commits: `type(scope): summary` (feat, fix, refactor, test, chore).
- Keep branches short-lived and single-purpose.

## Code style (only what differs from the defaults you already know)
- TypeScript strict mode, no `any`. Explicit return types on exported functions.
- ES modules (`import`/`export`), never `require`.
- Never swallow a rejected promise; throw a typed error instead.

But a markdown file is advisory. An agent can lose a line in a long file the same way a hurried human skips a checklist item, which is why Anthropic warns that a bloated CLAUDE.md gets ignored. So the load-bearing checks get promoted from prose to gates that do not depend on anyone reading them.

Give the agent a way to verify itself. The best-practices guide is direct about the failure mode: “Claude stops when the work looks done. Without a check it can run, ‘looks done’ is the only signal available, and you become the verification loop.” The test command in CLAUDE.md is what turns “looks done” into “the suite is green,” and it lets the agent close its own loop instead of handing you the job.

Make the critical checks deterministic with a hook. A hook is a script the harness runs automatically, not a suggestion the model may follow. A stop hook that runs your verify command turns advisory prose into a wall the turn cannot end against until it passes:

// .claude/settings.json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "npm run verify" }
        ]
      }
    ]
  }
}

Let CI be the backstop that does not care who wrote the code. The agent’s diff and the new hire’s diff hit the exact same required checks on the pull request. That is the whole trick, and it is why you do not need a separate quality regime for agents: if your definition of done was executable to begin with, it already governs them. The pipeline does not know or care whether a person or a model opened the PR. It only knows whether the checks are green.

This is also how you pay down that verification tax. It exists because humans became the slow step between fast generation and safe release. You do not fix a bottleneck by reviewing harder. You fix it by moving everything a machine can check into checks that run without you, so the scarce human review is spent only on what a person alone can judge: whether this is the right change, whether the design will hold, whether the code means what it says.

The objection, and the senior move

The pushback writes itself: this is a lot of process for a team that just wants to ship. It has the causality backwards. Every gate here exists so you can ship without holding your breath. The enjoyable part of working on a mature team is not heroics at 2 a.m., it is that a green check lets you stop thinking about a whole category of problem forever. You did not memorize the rule; a script holds it. You did not eyeball the diff for a missing test; CI did. As I put it after reading my own AI usage back to myself, a habit you encode beats a habit you intend, so you encode it.

And the encoded version is the only one that scales to what is coming. A definition of done in a senior engineer’s head cannot govern a fleet of parallel agent sessions, and if we are honest it was never applied evenly to the humans either. A definition of done that lives in make verify, a pull request template, a CI config, and a CLAUDE.md governs one engineer or fifty agents from the same files, with no drift and no meeting. That is the same instinct behind running our own site out of a git repo and a wall of checks, and it is the backbone of how we work with clients inside the Tarmac 10.

So write the definition of done as automation, once. Let the human and the agent both feed the same pipeline, and let the pipeline stay blind to which one did. Do that, and “works on my machine” stops being a joke and becomes a category error, because there is no “my machine” left in the definition. There is the check, and whether it is green. If you want help building that pipeline instead of just admiring it, our engineering teams do this for a living.

Let’s build something worth taking off.

Tell us what you’re building. We’ll assemble the senior team to ship it.