Directing agents
Ground the work in something checkable before anyone builds.
A spec turns a request into a contract an agent can verify its own work against. This page defines that habit and walks one real request from idea to shipped, verified change — spec, plan, failing test, code, and proof.
#From request to contract
A spec (specification) is the written answer to one question: how will we know this work is done and correct? It doesn't need a template or a tool — it needs to be checkable. The lightest useful spec is a sentence with acceptance criteria; the strongest is an artifact the agent can run its own output against. Everything in between counts.
#Give agents a concrete target
The single highest-leverage change in how you direct an agent: replace descriptions of what you want with an artifact the agent can check itself against.
- A failing test — the gold standard for behavior. Write (or have the agent write) a test that fails for exactly the bug or missing feature, then ask for the code that makes it pass. That's the difference between "I think it's fixed" and "the test passes."
- A screenshot or mock — for UI work, an image of the desired result beats paragraphs. The agent can compare its output against it and self-correct across iterations.
- A desired diff shape — "touch only these two files and add a flag here" bounds the blast radius before work starts.
- A sample of the output — for generators, transformations, and reports, show one correct example.
The principle underneath: an agent with a checkable target can verify its own work and iterate; an agent with only prose must guess what you meant and wait for you to notice. On real teams this habit hardens into machinery — a repo rule that every bug fix starts with a red test before any fix is written. You're learning the habit; a well-governed repo enforces it.
#Worked example: request to shipped
Not every request starts as code. This one starts in a planning meeting, from someone who has never written a line of code and never will — and it turns into the same kind of checkable spec, plan, failing test, and green run as any engineering-born request. The arc is identical whether the idea came from a stakeholder, a support ticket, or your own backlog.
The request
Someone turns the idea into a spec
The request can come from anywhere: a stakeholder in a meeting, a line in a support ticket, an offhand comment from an executive. Somebody still has to turn it into a spec before an agent or a human touches code. In a company with a product team, that's usually a product owner or a tech lead; working solo, that's you, wearing the same hat for five minutes. Either way the artifact is the same — acceptance criteria, a data model, edge cases — and the rest of this walkthrough shows how to build it.
The clarifying questions
This is the highest-leverage step in the whole arc: a few sharp questions before any spec gets written, because a vague request answered with a guess costs far more than a vague request answered with a question.
- Does this apply to existing users, or only new signups? — "Everyone. We can't have existing users sitting on an old agreement."
- What happens when the terms change — does a user have to accept again? — "Yes, every version needs its own acceptance. That's the whole point."
- Can a user decline, and if so, what happens? — "They just can't continue using the product until they accept. We don't need a fancy decline flow yet."
- Is the acceptance record ever edited or deleted? — "No — if we ever touch it, it stops being proof. It has to stay exactly as recorded."
Those four answers are the spec, mostly still in the stakeholder's own words. Writing them down is what turns "we need proof" into something an agent can build against.
The spec
Outcome: every user is blocked from using the product until they have accepted the current version of the terms, and each acceptance is permanently recorded.
Non-goals: writing or approving the legal text itself; building a richer flow for users who decline beyond blocking access.
Acceptance criteria
- A user who has not accepted the current terms version is blocked from using the product.
- Accepting records the user, the exact terms version, and a timestamp in UTC (Coordinated Universal Time, the single time zone servers should agree on).
- The timestamp is set by the server at the moment of acceptance, never supplied by the client.
- Publishing a new terms version requires every user, including ones who already accepted an older version, to accept again.
- A user who declines remains blocked from the product; no other action is taken.
- Acceptance records are never updated or deleted — every acceptance writes a new record.
- A user's acceptance history is the full, ordered list of every record they've ever produced.
Data model
Each acceptance is one record with these properties:
user_id · string · required | Identifies the accepting user. |
terms_version · string · required | Must match a published terms version; never blank. |
accepted_at · datetime · required | UTC, set server-side at the moment of acceptance — never accepted from the client. |
| the record itself | Append-only: no update or delete operation exists for it, by design. |
Edge cases
- A user accepts twice in a row (double-click, retry) — both writes succeed; two records with the same version is not an error.
- The terms version bumps while a user is mid-session — their next protected action is blocked until they accept the new version.
- A client-supplied timestamp arrives with the request — it's ignored; the server clock is the only source of
accepted_at.
The plan
- Write failing tests for the criteria that carry the most legal weight first: a rejected blank version, a server-stamped UTC timestamp, and append-only behavior.
- Implement a
TermsLedgerwith arecord_acceptance()method that writes one immutable record per call. - Wire the blocking check into the entry point every user passes through, comparing their latest accepted version to the published one.
- Run the tests until green, then commit.
The failing test
Before TermsLedger exists, this test names three of the criteria above — a rejected blank version, a UTC server timestamp, and one new record per acceptance:
from datetime import timezone
import pytest
from terms_ledger import TermsLedger
def test_rejects_blank_terms_version():
ledger = TermsLedger()
with pytest.raises(ValueError):
ledger.record_acceptance("user-1", "")
def test_stamps_utc_server_time():
ledger = TermsLedger()
record = ledger.record_acceptance("user-1", "2026-06-01")
assert record.accepted_at.tzinfo == timezone.utc
def test_appending_twice_yields_two_records():
ledger = TermsLedger()
ledger.record_acceptance("user-1", "2026-06-01")
ledger.record_acceptance("user-1", "2026-07-01")
assert len(ledger.records_for("user-1")) == 2
$ pytest tests/test_terms_ledger.py -q
# ModuleNotFoundError: No module named 'terms_ledger' — nothing to import yet
Build until it's green
The agent writes TermsLedger and reruns the tests after every change until they pass for the reason the spec names rather than by accident:
$ pytest tests/test_terms_ledger.py -q
# 3 passed in 0.01s
Delivered
$ git add terms_ledger.py tests/test_terms_ledger.py
$ git commit -m "feat: add TermsLedger for versioned terms acceptance"
The stakeholder who asked for "proof they accepted" never sees a line of code, but every claim in that ask is traceable: seven criteria above, tests that exercise the highest-risk three, one green run, and a commit message naming the behavior the tests prove.
Try it now: run the same arc on a real request
Pick one request sitting in your own inbox or backlog, phrased the way a non-engineer phrased it — a support ticket, a stakeholder ask, a line in a meeting note. Before any code, write the clarifying questions you'd actually ask, then the spec: acceptance criteria, a data model if the request touches stored data, and the edge cases that would embarrass you if nobody thought of them.
#Why this scales
One agent working from a vague goal produces one plausible-but-wrong diff, and you catch it. Fan that same vagueness out to five agents and you get five diffs drifting in five directions — the failure mode the industry named drift and built spec-driven development to kill: confident code that quietly solves the wrong problem because nobody grounded it in a real spec. A sharper spec makes delegation safe, and the same contract that makes a single agent reliable is what lets you trust a team of them — see AI agents & rules for the orchestration side.
The durable workflow, tool-independent: write the spec first, derive a plan, break it into atomic tasks, then let one agent or ten build with that spec as their shared memory. Every major tool shipped a flavor of this in 2025–2026 (GitHub Spec Kit, AWS Kiro, and others), but do not adopt a vendor's ceremony — adopt the habit.
#References
- How to write a good spec — for anyone writing their first specs: a practical, example-driven walkthrough of what makes a spec an agent can actually build from.
- Spec-driven development best practices — a working engineering team on the spec-first approach that grounds agents in a checkable contract before they build.
- AI agents & rules — the shared context layer specs belong to.