All articles

How to brief an AI so it cannot drift: vision anchors and done-conditions

The method we run on every engagement: a vision anchor stating what a project is not, done-conditions a machine can check, and a gate on every commit.

On this page
  1. The vision anchor
  2. Done-conditions, not done-feelings
  3. Gates on every commit
  4. A small worked example
  5. Where this fits

Give an AI agent a loose brief and it will do something. That is the problem, not the fix.

It picks a reasonable-sounding reading of what you asked for and runs with it. By the time you notice the reading was wrong, it has touched a dozen files. It invented a naming convention nobody agreed to. It quietly deleted a column it decided was spare.

Nobody lied to you. The brief left room to wander. An agent that can act at speed will wander at speed too.

The fix is not a longer prompt or a firmer tone. It is a brief with edges.

A written statement of what the project is, and what it is not. A set of conditions a machine can check, instead of a feeling that it looks about right. And a gate that blocks anything that fails those conditions.

We run this on every engagement, and on our own codebase. None of it is exotic. All of it is the difference between an agent that stays on task and one you have to babysit.

The vision anchor

A vision anchor is a short document, written before anyone touches code. It states what the project is. It also states what the project is not.

The "is not" half does the real work. Most briefs describe the destination and leave the boundaries implied. An agent cannot infer an implied boundary. It only has the words in front of it.

A useful anchor names the shape of the output. It names the inputs the agent may touch. And it names what is out of scope, said as plainly as the goal.

"This script does not deduplicate records" beats a paragraph about data quality. An agent can check one line of output against it. So can a reviewer.

Write the anchor once. Refer back to it whenever a request starts to grow.

When a follow-up says "while you're in there, could you also...", the anchor answers in under a minute. This project, or the next one.

Done-conditions, not done-feelings

"Improve the import script" is not a done-condition. Nobody, human or agent, can check it against anything.

It invites whichever reading feels most complete at the time. That is exactly the drift the anchor was meant to stop.

A done-condition passes or fails without judgement. The output file exists at a named path. Its header row matches the agreed schema exactly. The row count in the output equals the count of non-blank rows in the input. A row missing a required field stops the script with an error naming that row, instead of being dropped in silence.

You check each of those by running something. Not by reading the code and deciding it looks fine.

Writing done-conditions before the work starts buys a quieter benefit too. It forces you to decide what finished means, before you rely on an agent's judgement to decide it for you.

Most scope drift is not the agent being careless. It is the brief never having given the agent an edge to respect.

Gates on every commit

A done-condition that only gets checked when someone remembers is a done-condition that gets skipped. Deadlines do that, to agents and to people alike.

The condition needs to live as a test that runs on its own, every time. A commit that fails it does not land, however reasonable the change looks.

Teams skip this part because it feels like process for its own sake. It is not.

An agent working fast will produce a change that reads as correct and quietly breaks a condition nobody was watching for. A gate does not care how confident the change looks. It runs the check. The commit passes or it does not ship.

That is the whole mechanism. Its value is that it needs nobody to notice anything.

The same pattern runs on the codebase behind this website. A rules file says what may and may not appear on a public page. A registry lists the only claims allowed to publish. An automated check scans every change before it ships and blocks anything that breaks either one.

Nobody re-reads the rules file before each commit. The gate does that instead.

A small worked example

Take the import script from above. The vague brief is "clean up this CSV and get it ready to import."

An agent handed that line has to guess the schema. Guess what counts as a duplicate. Guess what to do with a malformed row. A different guess each time is not a bug in the agent. It is the brief working against itself.

The anchor version states the goal directly. This script normalises a lead export into a fixed schema for import. It does not deduplicate leads. It does not enrich company data from an outside source. It does not touch any column outside the schema listed below.

The done-conditions attached to that anchor are checkable on sight. The output file exists at the agreed path. Its header row matches the schema exactly. Its row count equals the input's non-blank row count. Any row missing a required field produces a named error, not a silent drop.

The gate is a short test file. It asserts every one of those conditions and runs on each commit to that script.

Say a later change adds a helpful-looking deduplication step, from an agent or from a person in a hurry. The row count condition fails. The commit is blocked before it reaches anything real.

The anchor told the agent the boundary. The done-condition made the boundary checkable. The gate made it stick, without anyone standing over the work.

Where this fits

None of this replaces judgement. Someone still decides what belongs in scope and what a correct output looks like.

What it removes is the part where you re-apply that judgement by hand. On every single change. By someone tired enough to miss the one that matters.

Write the anchor once. Write the done-conditions once. Wire the gate once. The discipline runs itself from there.

Briefing AI on real work, and want a second pair of eyes on the brief before anything ships? Talk to us.

We will look at how a project is scoped and checked today. Then tell you plainly where it would drift.