Writing

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.