A fixed-scope engagement rarely dies on the estimate. It dies in the last week, when two people read the same sentence in the statement of work and disagree about whether it has happened yet.

The fix is mechanical. Take every acceptance criterion you can and write it as something that exits non-zero. Then done stops being a shared feeling about a document and becomes a build that either produces the release or refuses to.

The three criteria below turn up in almost every product statement of work. They are illustrative, written first the way contracts usually phrase them and then as checks. The pattern is the point, not the particular tools.

Acceptance is the last moment anything is enforceable

The clearest writing on what acceptance means is not in software. It is in federal procurement, where acceptance constitutes acknowledgment that the supplies or services conform with the contract’s quality and quantity requirements.[2] Under the standard fixed-price inspection clause, that acknowledgment is conclusive, except for latent defects, fraud, or gross mistakes amounting to fraud.[1]

Conclusive. Most software contracts are not federal contracts, but the mechanics are the same everywhere the price is fixed: there is a moment when the buyer says this is the thing, and after that moment the argument is over.

Fixed price is more exposed here than hourly, not less. The procurement theory is direct about why: fixed price gives strong effort incentives, and gets expensive as design completeness falls, because every deviation has to be renegotiated as a change order.[3] Renegotiation needs a baseline. If the baseline is prose, the renegotiation is an argument about English, held between two parties who each have money riding on their reading.

Agile’s answer to this is the Definition of Done, “a formal description of the state of the Increment when it meets the quality measures required for the product”. An item that does not meet it cannot be released or even presented at the Sprint Review.[4] That is the right rule. What the guide does not say is where the definition lives, and in practice it lives in a wiki page that nobody opens during the week it would have mattered.

So put it somewhere nobody has to remember to open.

Three criteria, rewritten as checks

“The app is fast on mobile.” As written, nobody can fail it, which means nobody can pass it either. As a check, it becomes a performance budget run on every build. Lighthouse CI does this directly: an assertion set to the error level is checked on each run and a failure produces a non-zero exit code.[5]

{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }]
      }
    }
  }
}
Illustrative. A 'fast on mobile' clause written as Lighthouse CI assertions, so a slow build fails instead of shipping

The two numbers are the whole negotiation. Arguing about 0.9 against 0.85 in week one takes ten minutes. Arguing about “fast” in the last week takes the relationship.

“Existing integrations keep working.” A common line in any build that exposes an API to the client’s other systems. As a check: a CI step compares the API specification against the last released version and exits non-zero on a breaking change, such as a removed field, a narrowed type or a deleted endpoint. The contract stops saying “compatible” and starts saying “passes the compatibility diff against release 1.4”.

“Every database migration can be rolled back.” Easy to promise, rarely tested until the night it is needed. As a check: CI applies every migration to an empty database, then rolls each one back, and fails if any down step errors. Nobody has to remember to test the rollback, because the build does not produce a release until the rollback has run.

None of the three has a fallback, and that is deliberate. A check that fails quietly into a warning is guarding against exactly the thing it was written for: a step that did not happen and shipped looking almost fine. The failure these catch is rarely a bug anyone notices at the time. It is a step that did not run, with nothing red to show for it, which is what a rule enforced by people tends to do under a deadline.

Writing the check finds the criteria that were never real

Try to turn “all forms validate input” into a check and you discover that nobody ever agreed what valid means. Which fields, which formats, which message on failure, and whether the server re-checks what the browser already checked. The sentence sat in the contract for months and meant nothing until somebody had to make a machine refuse it.

The same thing happens one level down, in the check itself. A criterion the check treats as optional quietly turns “every endpoint requires authentication” into “every endpoint that somebody remembered to tag requires authentication”. The words in the contract did not change. What they enforced did.

That is the shape of the practice. The list of executable criteria is never finished, and writing one down is mostly how you find the next one that is not.

A check that can be skipped is not a gate

git commit --no-verify bypasses the pre-commit and commit-msg hooks.[6] One flag, typed by a tired person at the end of a long day, and every local hook in the repository is advisory. Hooks are a convenience for the author. They are not the gate, and treating them as one is how a team ends up believing in a check that has not actually run in months.

The gate sits in two places nobody can route around:

  • The merge. A protected branch can require status checks to pass before anything is merged into it.[7]
  • The deploy. On Cloudflare Pages, for example, a non-zero return code from the build command marks the build failed, and assets are uploaded only on a zero exit.[8] That is why a check belongs inside the build command and not beside it. A failing check then produces no new release at all, rather than a release with a warning in the log.

The part of that a buyer should care about: it applies to the vendor too. A studio cannot skip its way past a criterion that lives in the build command, at 2am on the last day, when skipping it would be very convenient.

What you cannot turn into an exit code

Much of what a client actually cares about. The copy sounds like the brand. The empty state does not feel broken. The onboarding flow makes sense to someone who has never seen it.

Accessibility is the sharpest case, because it looks more automatable than it is. The W3C is blunt: evaluation tools cannot determine accessibility, they can only assist in doing so, and human judgement is required.[9] A green automated run is evidence, not a conformance claim, and a contract that treats it as one has written a criterion that passes while the product fails.

The wrong response is a check that measures a proxy and reports on the thing. The right one is to name the human gate with the same precision: who looks, at what, before which step, and what happens when they say no.

Suppose the statement of work says the onboarding copy is approved by the client’s head of marketing. No machine can judge the copy. A branch rule can require an approving review, and can require it from a named code owner, before the change merges.[7] The build has no opinion about whether the copy is good. It enforces that the named person looked.

That is the general form worth stealing. When you cannot automate the judgement, automate the evidence that it took place.

What this costs

Four things, none of them free:

It moves the argument to week one. Turning “responsive on mobile” into something a machine can fail means deciding, before any code, which breakpoints and which pages and what counts as broken. Some buyers experience that as a studio being slow to start. It is the same conversation either way. The only question is whether you have it while the price is still negotiable.

You pay for the checks. They are inside the fixed price, not beside it, and on a real engagement they are a line item worth seeing rather than discovering.

A loud check blocks you at the wrong hour. A performance budget with no warning tier will stop a release on the day a third-party script gets heavier, whether or not anyone on the team touched it. That is the trade: a build that refuses at an inconvenient moment, in exchange for never finding out from a customer.

A check can be confidently wrong. It enforces what you wrote, not what you meant. A breaking-change detector that reads a renamed field as a deletion will block a release that breaks nobody. Fixing the check is usually one line. Noticing that the check is wrong rather than the code is the expensive part. The harder version is a rule nobody can pass yet: land it as an error and the build stays red, land it as a warning and it quietly becomes optional. Either choice is defensible, and the choice should be written down.

If you are buying a fixed-scope engagement

Three questions, before signing, and they take about five minutes:

  1. Which acceptance criteria run in your pipeline, and can I see one fail?
  2. What does the build do when one of them fails, and who can override it?
  3. Which criteria need a human, and whose name is against each one?

A studio that cannot answer is not automatically a bad studio. It does mean the acceptance conversation is happening in the last week instead of the first, and by the last week acceptance is conclusive.[1]

We quote fixed scope, and we refuse it for genuinely exploratory work. The refusal is only possible because on the work we do take, done is something the build can hold an opinion about. Where it cannot, we say so in the statement of work and put a name next to it.

A definition of done that has never failed anything is not a definition. It is a description.

Sources

  1. 52.246-2 Inspection of Supplies-Fixed-Price - Federal Acquisition Regulatory Council , accessed

    Supports: Under the standard fixed-price inspection clause, acceptance is conclusive except for latent defects, fraud, gross mistakes amounting to fraud, or as otherwise provided in the contract.

  2. FAR 46.501 General (Acceptance) - Federal Acquisition Regulatory Council , accessed

    Supports: Acceptance constitutes acknowledgment that the supplies or services conform with applicable contract quality and quantity requirements.

  3. Incentives versus Transaction Costs: A Theory of Procurement Contracts - The RAND Journal of Economics (paywalled at the publisher; linked copy is an accessible mirror)

    Supports: Fixed-price contracts give strong effort incentives but become expensive as design completeness falls, because every deviation has to be renegotiated as a change order.

  4. The 2020 Scrum Guide - Ken Schwaber and Jeff Sutherland , accessed

    Supports: The Definition of Done is a formal description of the state of the Increment when it meets the quality measures required for the product, and an item that does not meet it cannot be released or even presented at the Sprint Review.

  5. Lighthouse CI Configuration - Google Chrome (Lighthouse CI) , accessed

    Supports: A Lighthouse CI assertion set to the error level is checked, printed to stderr, and on failure produces a non-zero exit code; one set to warn is printed but does not.

  6. git-commit Documentation - Git , accessed

    Supports: The --no-verify option bypasses the pre-commit and commit-msg hooks.

  7. About protected branches - GitHub , accessed

    Supports: A protected branch can require status checks to pass and can require a specific number of approving pull request reviews, optionally from code owners, before a change is merged.

  8. Build configuration - Cloudflare , accessed

    Supports: On Cloudflare Pages any non-zero return code from the build command causes the build to be marked as failed, and assets are uploaded only on a zero exit code.

  9. Selecting Web Accessibility Evaluation Tools - W3C Web Accessibility Initiative , accessed

    Supports: Web accessibility evaluation tools cannot determine accessibility, they can only assist in doing so, and human judgement is required.