Writing specs that agents can actually follow
4 min read
Most advice about writing specs for coding agents is about prose — be specific, give examples, state constraints. That advice is fine and it is not the bottleneck once you are running more than one agent. At that point a spec is a graph, and the useful question is whether it has properties you can check before anything runs.
DevPilot validates a plan before dispatch rather than discovering its problems afterwards. The checks are the interesting part, because each one corresponds to a way multi-agent work actually fails.
A plan is a DAG, and it has to actually be acyclic
export function validateDAG(
tasks: ParsedTask[],
edges: ParsedEdge[],
config?: DAGValidatorConfig
): ValidationResult
Tasks with dependency edges between them. The validator topologically sorts them, and a cycle is an error rather than a warning.
This sounds obvious and it is the most common defect in a plan written by a human in a hurry. "Task C needs the interface from B, B needs the schema from A, and A needs C's migration" is easy to write in prose and impossible to execute. On a single agent working sequentially you discover it three hours in. With a wave planner it fails immediately, at parse time, before anything is dispatched.
The lesson transfers even if you never use a tool like this: write your dependencies down as edges, not as narrative order. Prose hides cycles. A list of "X depends on Y" makes them visible, and you can find them yourself in a minute.
Empty and degenerate plans are errors, not no-ops
if (tasks.length === 0) {
errors.push({
code: 'EMPTY_PLAN',
message: 'Wave plan contains no tasks',
detail: 'A valid wave plan must contain at least one task',
});
A plan that produces no tasks is almost always a parse failure or a prompt that did not land, not a genuine "nothing to do". Treating it as a successful empty result means the failure surfaces as silence — an agent that finished instantly and did nothing, which reads as success on every dashboard.
Any pipeline that turns natural language into structured work needs this check. The degenerate output of a broken extraction step is usually empty, and empty is indistinguishable from done unless you make it an error.
File ownership is what makes parallelism safe
The validator has a strictFileOwnership mode, and it is the check that matters
most for running several agents at once.
Two tasks in the same wave that both claim src/auth/session.ts will produce
two diffs against the same file. Each may be individually correct. Together they
conflict, and the conflict surfaces at merge — the most expensive place to find
it, after both agents have spent their tokens.
Declaring which files a task owns turns that from a merge-time surprise into a plan-time error. It also gives you the honest answer to how many agents you can run in a wave: as many as have disjoint file sets, and no more.
The practical consequence for how you write a spec: name the files. Not "update the auth module" — the paths. It feels over-specified and it is the single highest-value thing you can add, because it is the only part of a task description that can be mechanically checked for conflict against its siblings.
Tasks have to fit in one context
The constraint nobody writes down: a task an agent cannot hold in one context window is not a task, it is a project. It will be attempted, partially completed, and reported as done.
There is no clean automated check for this — it depends on the model, the codebase, and how much surrounding code the agent needs to read. The heuristic that has held up: if you cannot state the acceptance criteria in three bullet points, the task is too big, and the fix is to split it rather than to write a longer description.
Adherence is a structural property, not a prompting one
The framing that changed how we write these: you do not get adherence by asking for it. "Follow the spec exactly" in a prompt is weak. What produces adherence is a task small enough to fit, with a named file set, with dependencies satisfied before it starts, and with acceptance criteria concrete enough that finishing is checkable.
Each of those is a property of the plan's structure. All of them can be verified before a single token is spent — which is the whole reason validation runs at parse time and not at review time.
The same instinct shows up elsewhere in this system: work is only considered delivered when it lands in a durable queue row, not when something reports that it sent it. Check the structure, not the intention.
What this does not do
Validation proves a plan is executable. It says nothing about whether it is good — whether the decomposition is sensible, whether the waves are the right width, whether the critical path is as short as it could be. A perfectly valid plan can still be a bad one.
That judgement is currently human. Making it measurable is the harder problem, and it is what a benchmark suite is for: the same task, decomposed differently, scored on outcomes. We have the harness and not yet the results, so anything we told you here about which decompositions win would be invention.
