How to Give an AI Agent Tasks: One Feature at a Time, Feature List and Done Criteria
Give an AI agent one task at a time: exactly one feature is in progress, it has observable behavior and an executable verification command, and anything noticed along the way goes into a backlog instead of being fixed. The state of all features lives in a machine-readable list (for example, feature_list.json), and only a verification script, not the agent, can mark a feature as done.
Why an agent tries to do everything at once
You ask the agent to build a sign-up flow. Two hours later the diff has twelve changed files and eight hundred new lines: a user model, email verification, a connected mail service, password hashing, a refactored middleware and moved tests. Everything is tightly coupled, and not one scenario passes end to end. The next session opens this code and does not know where to start.
The cause is not a "bad model". A human has a sense of "enough for today" and ordinary fatigue, and these limit the scope of the work on their own. An agent has neither. Adding "and while I'm at it, I'll fix the error handling" costs it a few dozen tokens, but the cost to the project is real: attention gets spread thin, coupling grows, and nothing gets finished.
There is also simple arithmetic. Let C be the useful capacity of the context and k the number of tasks the agent holds at once. Each task gets roughly C/k of the reasoning budget. When this value drops below what is needed to finish one task, no task gets finished. That is why the right answer to how to give tasks to an AI agent starts not with the wording of the prompt but with a constraint: one task in progress.
The one-feature rule (WIP=1)
WIP stands for work in progress, the number of tasks in progress. For an agent it is set to one: at any moment exactly one feature is active, and the next one starts only after the current one passes verification. The rule is written into the project instructions file (AGENTS.md or CLAUDE.md) so the agent sees it in every session:
## Working rules
- At any moment EXACTLY ONE feature is in the `active` state.
- Start the next feature only after the current one has moved to `passing`.
- Do not "while I'm at it" refactor, rename, reorganize directories,
or fix unrelated bugs. If you find a problem, write it to `BACKLOG.md` and move on.
- Changes outside the scope of the active feature count as a violation, even if they are useful.
The last rule matters more than it seems. The agent sincerely considers its "while I'm at it" changes useful, and often they are. But a useful change outside the scope still mixes two tasks in one diff, and then the two can no longer be rolled back or verified separately.
BACKLOG.md: where the "while I'm at it" goes
The urge to "fix it while we're here" cannot simply be forbidden; it needs a legitimate outlet. That outlet is the BACKLOG.md file: the agent writes the problem it noticed in one or two lines and returns to the current feature. Nothing is lost, and the scope does not grow. Without a backlog, the one-feature rule will eventually be circumvented, because silently leaving a bug seems worse to the agent than breaking the rule.
# BACKLOG.md
- [ ] middleware/errors.py: different error formats in /auth and /orders
- [ ] tests/: user fixtures are duplicated in three files
- [ ] is email verification needed at sign-up? — ask
Done criteria: "done" is a command
The second half of the problem is that the agent decides by itself when a task is finished. "The code looks fine", "the function exists", "there are no compile errors" are all impressions, not facts. The done criterion for an agent must be an executable command with an unambiguous result: pass or fail.
F01: user registration
verification: pytest tests/e2e/test_auth.py::test_register_flow
state: active
The difference between a weak and a strict criterion is easy to see in examples:
| Weak criterion | Strict criterion |
|---|---|
| The code compiles | pytest tests/e2e/test_cart.py -q passes |
| There are no syntax errors | curl to /cart returns status 200 |
The addToCart function exists | After adding an item, GET /cart shows it and the correct total |
| It looks right | npx playwright test cart.spec.ts passes |
A good verification command is deterministic (the same code gives the same result), runs without human involvement, and checks behavior from the outside, the way a user or an API client would see it. If you cannot write such a command, that is a sign the task is not clearly formulated.
Decomposition: how big a feature should be
The most frequent question after "one feature at a time" is what counts as a feature. A task that is too broad will not finish in one session; a task that is too narrow drowns in the overhead of startup, context and verification.
- Too broad: "implement the cart". That is a week of work and a dozen scenarios.
- Just right: "the user can add an item to the cart and see it there". One observable behavior, one verification scenario.
- Too narrow: "create a quantity field in the CartItem model". That is an implementation step, not a behavior; testing it on its own makes no sense.
A handy size check: one feature = one agent session = one meaningful commit = one end-to-end (E2E) scenario. If a feature does not fit into a session, split it further. If you cannot write a separate scenario for it, it is too small, so merge it with a neighbor.
Example decomposition of the "cart" task
A broad requirement is split into atomic units, each with one behavior and its own verification command:
- Add an item to the cart and see it in the list.
- Change the item quantity, and the total is recalculated.
- Remove an item from the cart.
- A guest's cart is kept after logging into an account.
- An item that is out of stock cannot be added, and a clear error is shown.
Final check of the decomposition: no unit requires touching another unit "on the side". If item 4 requires rewriting item 1, the order or the boundaries were chosen badly.
Feature list: feature_list.json
The one-feature rule keeps order within a session. But who remembers between sessions what is already done? A note in chat, "auth and catalog are done, the cart is almost ready, but there are bugs", gives a new session nothing: what "almost" means, which tests pass, what blocks payments is unknown. Each new session is like a new driver of the same car: if the notes lived only in the head of the previous one, the new driver knows nothing.
So the state is moved outside, into a machine-readable file. In the course this is .agent/feature_list.json:
[
{
"id": "F01",
"behavior": "The user registers with email and password and is logged in right away",
"verification": "pytest tests/e2e/test_auth.py::test_register_flow",
"state": "passing",
"evidence": "a3f9c12: 1 passed in 2.31s"
},
{
"id": "F02",
"behavior": "The user adds an item to the cart and sees it in the list",
"verification": "pytest tests/e2e/test_cart.py::test_add_item -q",
"state": "active"
},
{
"id": "F03",
"behavior": "Paying for an order by card",
"verification": "pytest tests/e2e/test_payment.py -q",
"state": "blocked",
"blocker": "no test keys for the payment gateway"
}
]
Each entry has three required fields: the behavior description (observable from outside), the verification command (executable without a human) and the state. An optional fourth field is evidence: the commit hash and the verification output, proof that the move to passing really happened.
Why JSON and not a Markdown list? It is read not only by the agent but also by scripts: a planner picks the next feature in not_started, a verifier takes the verification command, a session report is assembled automatically, and the distribution of states gives health metrics for the project. The feature list is the backbone of the harness, not a memo.
Four states and the allowed transitions
| Transition | When |
|---|---|
not_started → active | The feature is taken into work; no other feature is active |
active → passing | The verification script exits with code 0 |
active → blocked | An external blocker; the reason is recorded in blocker |
blocked → active | The blocker is removed |
There are no other transitions. In particular, there is no path to passing that bypasses verification, and there is no "almost done" state.
The passing gate: the agent can request, but not declare
The central rule of the whole scheme: the agent does not edit the state field by hand. It can run the verification, but it cannot declare the result itself; the script does that. The difference between "the agent reported" and "the script confirmed" is the difference between a claim and a fact. The rules are written into the instructions file too:
## Feature list rules
- File: `.agent/feature_list.json`. This is the single source of truth for scope.
- Exactly one feature is in the `active` state.
- You do NOT edit the `state` field manually. It is changed by `scripts/verify.sh`.
- To move a feature to `passing`, run `./scripts/verify.sh F02` and get exit code 0.
- If a feature is blocked, move it to `blocked` and always specify `blocker`.
- New features are added only to `not_started` and only with a verification command.
The script itself checks on two levels: first the project-wide run (the feature must not break the rest), then the command of the specific feature. Local success with a global regression does not count as success.
#!/usr/bin/env bash
# The only component allowed to set passing.
set -uo pipefail
FID="$1"
FILE=".agent/feature_list.json"
CMD=$(jq -r --arg id "$FID" '.[] | select(.id==$id) | .verification' "$FILE")
if [ -z "$CMD" ] || [ "$CMD" = "null" ]; then
echo "FAIL: no verification command for $FID"; exit 2
fi
echo "== global check =="
make check || { echo "FAIL: global check is red, $FID cannot be passing"; exit 1; }
echo "== feature check: $CMD =="
if OUT=$(eval "$CMD" 2>&1); then
SHA=$(git rev-parse --short HEAD)
LAST=$(printf '%s\n' "$OUT" | tail -n 1)
TMP=$(mktemp)
jq --arg id "$FID" --arg ev "$SHA: $LAST" \
'map(if .id==$id then .state="passing" | .evidence=$ev else . end)' \
"$FILE" > "$TMP" && mv "$TMP" "$FILE"
echo "PASS: $FID"
else
printf '%s\n' "$OUT"
echo "FAIL: $FID"; exit 1
fi
A rule "do not edit state" in the text is still a request. To make it a constraint, the write access to the file can be closed mechanically: with a CI check that compares evidence with the commit history, or with restrictions on the agent's permissions in your tool. The principle is the same as everywhere in the harness: an important rule is backed by a mechanism.
How to measure whether the agent finishes tasks
A tempting metric is the amount of code per session. In practice it works the other way: a large diff more often means the agent scattered across several directions. Count features in the passing state, not lines.
A useful metric from the course is VCR (Verified Completion Rate): the number of verified tasks divided by the number of activated tasks. The rule is simple: while VCR is below one, new tasks may not be activated. This is the back pressure the harness applies to the agent, and it also makes unfinished work visible as a number rather than a feeling.
Common mistakes when assigning tasks to an agent
- A task without a verification command. "Build the sign-up" without a scenario: the agent decides for itself where to stop, and decides wrongly.
- Several features in one request. "Build the cart and payments" is two active tasks, and neither will be finished.
- State only in chat. Progress written in a conversation disappears when the session ends. It must live in a file.
- Self-report instead of verification. "The agent said the tests passed" is not the same as the exit code of a script.
- No backlog. Forbidding "while I'm at it" without a place to write down what was noticed means the agent will work around the rule.
When one feature at a time is no longer enough
The scheme "one feature, one session, one check" works well for a single agent in a single loop. When there are many tasks and independent branches appear, it is extended into a system of several loops: a separate planning node, implementation nodes, a verification node, and edges between them, parallel or conditional, with a return to implementation if the check fails. But even there, each implementation node works by the same rule: one task, an executable done criterion, state outside the agent's head. The graph does not cancel the discipline of one feature; it is built on top of it.
An important warning from the same analysis: a loop honestly optimizes the metric it is given. If the done criterion is chosen badly, for example "the ticket is closed" instead of "the problem is solved", the agent learns to satisfy the criterion rather than the task. That is why the wording of the behavior and of the verification command deserves more time than it seems to.
Summary
In short, to give an AI agent tasks: one at a time, with observable behavior and an executable verification command, with a backlog for everything noticed along the way, and with a feature list in a file where the move to "done" is made by a script, not by the agent. The brakes on an agent are set by the harness; the agent will not switch them on itself. The WIP=1 rule, feature_list.json and verify.sh are covered in practice in the harness engineering course, using an end-to-end project as the example.
FAQ
Can I give the agent several tasks at once if they are small?
You can put them into a feature list, but only one should be in progress at a time. The agent takes the next one only after the current one passes verification, so each task gets finished.
What if I cannot write an automatic check for a task?
Usually this means the task is phrased as an action rather than a behavior. Rephrase it through what a user or an API client will see; for the visual part, browser e2e tests will do.
Who writes feature_list.json, the human or the agent?
It is convenient to draft the initial list together: the agent proposes a decomposition and the human edits it. After that the agent may add new features, but only in the not_started state and with a verification command.
How is feature_list.json different from tasks in a tracker?
A tracker is designed for people; a feature list is designed for machine reading. Each entry has an executable command and a strict state that only the script changes. They can be synchronized, but the agent needs a file in the repository.
Does the one-feature rule slow the work down?
Sessions become shorter, but the share of tasks brought to completion grows. The time that used to go into untangling half-finished code from three mixed features is saved.
This topic is covered in depth with practice in the course “Harness Engineering: Engineering Reliability for AI Agents”.
- feature_list.json for AI Agents: WIP=1, Feature Status and verify.sh Verification
- Graph Engineering for AI Agents: When a Single Loop Is Not Enough and You Need a Task Graph