Your Agent Plan Should Be Longer Than Your Design Doc
A plan's job is to pre-decide what the agent would otherwise decide silently. The document skeleton that does it, traced through one shipped change.
My repository for a fleet and logistics platform keeps its agent-facing documents in two directories: docs/superpowers/specs/ holds design documents, docs/superpowers/plans/ holds implementation plans. Both come out of Claude Code sessions I drive: I approve the spec, then the plan is written against it. I counted the two directories with wc -l: the 36 specs total 6,765 lines, a mean of about 188; the 45 plans total 38,639 lines, a mean of about 859. The executable plan runs four to five times longer than the design it implements.
| Document type | Count | Total lines | Mean |
|---|---|---|---|
Design spec (docs/superpowers/specs/) | 36 | 6,765 | ~188 |
Implementation plan (docs/superpowers/plans/) | 45 | 38,639 | ~859 |
The inversion is deliberate. The design answers what are we building and why. The plan answers what will go wrong, and what is the agent allowed to do about it, and the second question needs more words. A plan’s job is not to describe the work; it is to pre-decide the choices a coding agent would otherwise make silently, at the moment it is least equipped to make them.
If you already run a coding agent (Claude Code, Codex, Cursor) on a real codebase, already write plans, and still find sessions that end in success while quietly dropping the hard part, what follows is aimed at that failure. I walk the skeleton my 45 plans share, section by section, through one change I shipped: a cached road-distance matrix for a route-optimization worker. The skeleton is stable enough to name: 44 of the 45 plans track steps as - [ ] checkboxes, 26 carry a File Structure table, and 10 carry a Global Constraints section.
Five failures with the same root cause#
Every one of the failures below has turned up in my own sessions.
- The silent downgrade. The agent cannot get the integration test running in this environment, writes a unit test instead, reports success, and never mentions that the negative case is now uncovered. A June 2026 paper names the wider pattern building to the test (opens in new tab): the agent satisfies the signal that gets checked, and whether the requested behaviour exists stays an open question.
- The helpful refactor. Two handlers share a query shape, so the agent merges them, and the PR now touches a well-tested handler nobody asked it to touch.
- The invisible violation. The agent obeys a repo rule literally and breaks it in effect, because the rule was written at the wrong level. The closing section is one I shipped.
- Stale coordinates. The plan was written against a file that has since been rebased, and the agent follows line 254 into the wrong place with full confidence.
- Scope drift with no record. Halfway through, the agent decides something is out of scope. Review finds out; a written reason never existed.
None of these are model-capability failures. In each one a decision had to be made, no written instruction covered it, and the agent picked an answer without flagging the pick. The six sections that recur in my plans exist to make those decisions in advance:
| Plan section | Failure it closes |
|---|---|
Global Constraints | The invisible violation |
File Structure | The helpful refactor |
Deliberately not in this plan | Scope drift with no record |
Notes for the implementing engineer | The silent downgrade |
Concurrent work — resolved | Stale coordinates |
Acceptance verification | The completion claim without evidence |
The running case#
The change: cache one road-distance matrix over the distinct-address universe in S3, so repeat optimization jobs make zero calls to the routing engine. The call being removed is an OSRM Table request (opens in new tab), which returns all-pairs durations for a coordinate set, and distances when annotations=duration,distance is requested.
The reason it needed a plan is the budget. The route-optimization worker runs as an async Lambda under Lambda’s 900-second timeout (opens in new tab), the hard maximum. At thousand-stop scale, the matrix build alone was estimated at 5 to 7 minutes of that budget. The cache buys headroom. It also carries a silent failure mode: if the cached matrix is wrong, routes change, and nothing errors. High value plus silent failure is exactly the combination where a plan earns its length.
Four artifacts run through everything that follows:
- The design spec: 184 lines.
- The implementation plan: 1,330 lines, five tasks.
- The change I shipped, behind a dev flag (#944).
- The fix I shipped three days later (#948): four lines of source.
The fourth artifact is where the story ends.
The document chain#
The documents come out of a fixed chain of Claude Code skills, the superpowers (opens in new tab) set. A skill here is a Markdown instruction file the agent loads when invoked (opens in new tab), so the chain is reproducible by anyone:
superpowers:brainstorminginterrogates intent before anything is written.- The design I approve lands in
docs/superpowers/specs/<date>-<slug>-design.md. superpowers:writing-plansturns the spec into a task-by-task plan underdocs/superpowers/plans/.superpowers:using-git-worktreesisolates execution; several of my plans literally open with a worktree-and-branch task.superpowers:subagent-driven-developmentorsuperpowers:executing-plansexecutes; a subagent runs each task in its own context window (opens in new tab).superpowers:verification-before-completiondemands evidence before the completion claim.
Every plan opens with the same banner, verbatim from the writing-plans plan template; the executor chooses between the two named skills and nothing else:
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
This is a different artifact from Claude Code’s built-in plan mode (opens in new tab), which is a session-scoped research phase that proposes changes without editing source. The plan file survives the session, gets reviewed like code, and can be executed again.
Readers of my earlier Spec Kit post should swap vocabulary at the door: Spec Kit’s plan.md is its architecture document (the spec, in the vocabulary here), and its tasks.md is the executable breakdown (the plan here). There I also advised against specifying every implementation detail. The advice is about the design document, and the plan is not the design document: the extra length below goes to constraints, exclusions, stop conditions, interfaces, and the code the plan pre-writes. None of that re-litigates the architecture, which is what the advice warns against. And in the terms of the model-tier-versus-harness post, everything below is harness work: the model stays the same, and the documents around it decide more.
Global Constraints#
The plan header opens with the rules a competent agent cannot derive from the code in front of it, because they live in CI, in a guard script, or in an incident I had before the session started. From the running plan:
No raw DynamoDB client imports. A CI guard (
scripts/check-raw-ddb-imports.sh) rejects them outside an allowlist. All DynamoDB access goes through adynamodb-toolboxEntity.
entityTypemust be set in every entity’s schema defaults. Omitting it has previously caused enforcement logic to silently no-op.
Commit after every task. Never use
--no-verify.
Some of these duplicate my standing rules files, the layer covered in the model-agnostic setup post. The duplication is deliberate: the plan restates the rules this specific change is most likely to trip, so they sit in the executor’s context at decision time rather than in a file it may never re-read.
The section itself is not my invention. The writing-plans SKILL.md (opens in new tab) carries a Global Constraints section in its plan template, alongside checkbox steps and an explicit executor choice, and it forbids vague directives. The section lands in a plan when the spec has project-wide requirements to copy in; plans without them drop it, which is why only 10 of my 45 carry one. The skeleton is portable across repositories; the contents are earned locally, and most of mine came out of incidents.
One constraint outranks the rest, and the plan says so in a single sentence:
The cache must never fail a job. Every error path falls back to the current uncached behaviour. This is the single most important constraint in this plan.
A constraints list where everything is equally important is a list where nothing is, so I mark exactly one constraint as the one that wins ties. Here it gives the agent an ordering for the situations no list anticipates: when headroom competes with safety, safety wins, and the fallback path is always legal.
File Structure#
26 of my 45 plans carry a File Structure section: a table of every file the plan creates or modifies, with one sentence of responsibility each. The running plan splits the feature into a layout module holding only pure functions (address key to row index, blob encode and decode, matrix slice) and an orchestration module holding the S3 and DynamoDB work. Underneath the table, the plan explains why the split has that shape:
The pure/impure split is the load-bearing decision here. Slicing is where an index bug would be silent, and putting it in a module with no I/O means its test needs no mocks at all.
Naming which decision carries the weight is the step most easily skipped. It tells the agent which line it must not casually redraw, and at review it lets me tell an intentional structure from an improvised one. An agent tempted toward the helpful refactor now has to argue with a written sentence.
Deliberately not in this plan#
The negative scope section. The running plan closes four doors:
- Raising
MAX_OPTIMIZE_LOCATIONSabove 1500.- Raising the worker’s 2048 MB memory.
- Fixing the
null→0coercion (separate session, already running).- Enabling the flag in prod. That is a follow-up decision, taken on the dev numbers.
The first two close the resource knobs an agent might otherwise reach for when the budget gets tight. The last two carry their reasons inline: the coercion fix collides with a session I already had running, and enabling the flag is a decision I keep for myself, taken on the dev numbers.
Another of my plans shows the section doing harder work:
Any reaction to the count beyond logging it — no alarm, no threshold, no failing the job above some rate. The production rate has never been measured; this change is what makes measuring possible.
That is an exclusion arguing for itself. Nobody can set an alarm threshold on a rate nobody has observed, so the change ships the observation and stops there. Without the written reason, the same boundary reads as an oversight, and either I re-open it at review or the agent helpfully adds the alarm.
Both directions of scope failure end here: creep, because the door is named shut, and silent descoping, because cutting something mid-task now requires the same written form.
Notes for the implementing engineer#
The running plan does not carry this section, so both examples below come from other plans of mine. It is the section that most repays the writing time. Each note is three things at once: the risk, the signal it would show, and the retreat the agent is allowed to take.
The single highest-risk assumption in this plan is that
Orderrows reliably carryrouteId[…] If a future refactor of route assignment stops writing this field,Task 1’s helper silently returns fewer routes (not an error) — its three tests in Task 1 are the regression guard; keep them passing.
The shape is the value. The note names a break that produces no error, then names the artifact that would catch it. An agent reading this knows the three tests are the tripwire for the plan’s riskiest assumption, and that keeping them green is part of the task.
The second example compresses the whole method into one sentence:
If
pnpm cy:run:portalcannot drive a real WebSocket connection from Cypress in this environment, Task 4’s coverage may need to stay REST-only […] note this explicitly in the PR description rather than skipping the negative test silently.
The plan authorises the retreat and forbids the silent version of it. An agent that hits this wall does not invent a policy under pressure; it has one, and the cost of taking the retreat is a sentence in the PR description where I will see it. The review-side twin of this move is the verification packet from the review-load post: what the note pre-writes at plan time, the packet reports at review time.
This is the section aimed at the silent downgrade, the most common way a session ends green with a case uncovered.
Concurrent work#
Plans are written before the code exists, against a branch that keeps moving. I merged two other branches between this plan’s writing and its execution, so I amended the plan rather than regenerating it, under the literal heading Concurrent work — resolved:
Task 1 is implemented in adapted form (commits
7228e270+26509c7b): there is no-1sentinel and no null translation.
Line numbers quoted for
osrm-client.tsandroute-optimizer.tspredate the rebase — re-locate by symbol, not line.
The second amendment generalises past my repository. Coordinates in a plan should be symbol names, because line numbers age out with every rebase and an agent follows a stale one without hesitation. A plan that points at the retry wrapper in osrm-client.ts survives the rebase; a plan that points at line 254 sends the agent to whatever now lives there.
Acceptance verification#
The stop condition, written before the work. The section opens by declaring its genre:
Not a claim — a measurement.
The procedure is four log assertions across two runs of the same job. The first run must show the cold path: the matrix build and its tiling line. The second run must show newAddresses: 0 and no tiling line at all. Then comes the comparison that makes the whole feature falsifiable:
Compare the two runs’ resulting total distance. They must be identical — if the cache changed a route, the sentinel translation is wrong.
If step 3 shows any difference, stop and investigate before going further. A cache that changes results is worse than no cache.
An agent will not write its own stop condition. The Building to the Test paper (opens in new tab) states the disposition plainly: “The agent does not, on its own, validate what it ships as a user would.” My setup carries the answer at two levels. The verification-before-completion SKILL.md (opens in new tab) states the general rule as “NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE”, and the plan contributes the concrete evidence list: which log lines, which comparison, which difference aborts the task. A hook (opens in new tab) can run such a check mechanically at stop time; either way, the assertion itself comes from the plan text.
The task shape#
Inside the plan, all five tasks repeat one five-step sequence:
- Write the failing test.
- Run the test to verify it fails.
- Write the minimal implementation.
- Run the test to verify it passes.
- Lint and commit.
The order is red-green-refactor (opens in new tab) hard-coded into the document, which is why the executor never improvises it. Each task also opens with a header block: Files: (create, modify, test) and Interfaces: (consumes and produces, with actual signatures). The function signatures exist in the plan before any code does.
The less obvious plan-time decision is the test’s discriminating power. Task 1’s fixture lays out both matrices in one flat Int32Array (distances at offset 0, durations at offset n * n), and every expected cell is derivable by eye:
/**
* Pure layout maths for the cached distance matrix. These tests need no mocks
* because the module does no I/O — which is the point of the split.
*
* The universe is laid out on a line so every expected cell is known: the
* distance between rows i and j is |i - j| * 100 and the duration is |i - j|.
* A wrong index lookup therefore produces a wrong number rather than passing.
*/
const KEYS = ['a', 'b', 'c', 'd', 'e', 'f']
/** Universe of 6 points on a line: distance = |i - j| * 100, duration = |i - j|. */
function lineUniverse(): Int32Array {
const n = KEYS.length
const blob = new Int32Array(expectedBlobLength(n))
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
blob[i * n + j] = Math.abs(i - j) * 100
blob[n * n + i * n + j] = Math.abs(i - j)
}
}
return blob
}
The expectedBlobLength(n) the fixture calls is not in the snippet; it is declared in the plan’s Interfaces: block before any code exists. Nor did I lift the snippet from the finished test file. I copied it out of the plan, where it sits inside Task 1’s first step under the instruction to write the failing test. The docstring explaining the layout was written before the module it describes. A fixture of arbitrary numbers would let an off-by-one index lookup pass, because one arbitrary value reads as plausibly as another. This one cannot: any wrong index produces a visibly wrong value. Choosing that fixture is a plan-time decision, and it is the difference between a test that exercises the slicer and a test that can catch a wrong index.
The spec side#
The design document earns its 188-line mean by freezing decisions, so the plan does not re-litigate them. Two of its sections matter most for agent work.
Measured evidence carries the numbers together with their boundary. The running spec’s table of solver timings ends:
All figures measured 2026-07-30. Solver runs used a pre-built matrix pushed through the S3-pointer path, invoking the dev solver Lambda directly — the
optimize-workerpath itself was not exercised.
Declaring what was not measured stops a later reader, human or agent, from treating the table as broader evidence than it is.
Decisions (locked) freezes choices, including the YAGNI defaults. From another of my specs:
Override flag (YAGNI default): do not persist a separate “unverified” boolean on the order.
Without the locked entry, that boolean is exactly the field a thorough agent would helpfully add.
Where the method costs you#
A 1,330-line plan is expensive to write, and even with a skill drafting it, the real cost is mine: I review all 1,330 lines. The ratio pays on changes with a silent failure mode or a wide blast radius, and on little else; a rename does not need pre-registered failure modes. The line I use: if I cannot name a way the change could be wrong without erroring, the plan is probably overhead.
The gap the plan exists to close also grows with change size. SpecBench (opens in new tab) measures the distance between an agent’s visible validation tests and held-out tests, and reports it widening by 28 percentage points for every tenfold increase in code size. Small changes barely have the gap; large ones are mostly gap. Plan length earns itself along that curve, which is also why a one-size template misses in both directions.
The plan itself can be wrong. It is written before the code exists, against a moving branch; the Concurrent work — resolved section exists because that has already happened to me once.
The skeleton also travels better than the contents. Two of the six sections, Global Constraints and File Structure, come from the skill’s plan template; the other four are my own additions, and all six port to any repository. But the quoted constraints exist because I got something specific wrong once in mine. Copying them wholesale imports the words without the incidents, a close cousin of why copying Claude Code skills doesn’t work.
And writing a constraint down does not make it binding, which is the closing story.
A constraint followed to the letter#
I wrote this into the running plan’s Global Constraints, in bold:
entityTypemust be set in every entity’s schema defaults. Omitting it has previously caused enforcement logic to silently no-op.
I shipped the feature (#944). Three days later I shipped a four-line fix (#948), and its comment is the whole story:
.item({
pk: buildMatrixCachePK(graphVersion()),
sk: MATRIX_CACHE_SK,
+ // Schema `.default()`s are PUT defaults — UpdateItemCommand does not
+ // apply them, and a row born without entityType fails every later
+ // GetItem at the formatting step (measured on dev, 2026-08-03).
+ entityType: 'matrix_cache',
seq,
rows: keys.length,
})
The schema did set entityType, so my constraint, read literally, was satisfied. But the code I shipped wrote the pointer row through an update, and in DynamoDB Toolbox .default() on a non-key attribute is a put default (opens in new tab): PutItemCommand applies it, UpdateItemCommand does not, and an update default is a separate declaration. (On attributes tagged .key(), the same call acts as a key default instead.) The row was born without the field and failed every subsequent read at the formatting step. The constraint named the rule; the mechanism enforcing the rule lived one level lower, and that level had a second door.
The stronger form of the constraint names the mechanism: schema defaults apply on put only, so any update path must set entityType explicitly. That wording is not in any of my plans yet. It lives only in the comment on the fix, which is the same failure one level up: the incident taught the code and has not yet taught the document. That is where a guide about writing better plans has to end, because the plans get better the same way the code does, one incident at a time.
References#
- Extend Claude with skills (opens in new tab) - The SKILL.md format: a skill is a Markdown instruction file whose body loads only when invoked.
- Create custom subagents (opens in new tab) - Subagents run in their own context window with a custom system prompt, tool access, and permissions; the execution model behind subagent-driven plans.
- Choose a permission mode (opens in new tab) - Claude Code’s built-in plan mode: a session-scoped research phase, as opposed to a durable plan file.
- Hooks reference (opens in new tab) - Lifecycle hooks that run shell commands at fixed points; the mechanical way to run a stop-condition check.
- obra/superpowers (opens in new tab) - The skill set used throughout; the six skills named in the chain all live under
skills/. - superpowers: writing-plans SKILL.md (opens in new tab) - The plan template: a Global Constraints section, checkbox steps, bite-sized independently testable tasks, and an explicit executor choice.
- superpowers: verification-before-completion SKILL.md (opens in new tab) - The rule “NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE” and its identify, run, read, verify sequence.
- github/spec-kit (opens in new tab) - GitHub’s spec-driven development toolkit; its
plan.mdis an architecture document and itstasks.mdthe executable breakdown, the inverse of the spec/plan vocabulary here. - Configure Lambda function timeout (opens in new tab) - The 900-second (15-minute) maximum behind the running case’s fixed time budget.
- DynamoDB Toolbox: defaults and links (opens in new tab) -
.default()on non-key attributes as a put default, with.updateDefault()a separate declaration; the mechanism behind the closing fix. - OSRM HTTP API: Table service (opens in new tab) - The
/tableendpoint that returns all-pairs durations, and distances withannotations=duration,distance; the routing-engine call the cache removes. - SpecBench: Measuring Reward Hacking in Long-Horizon Coding Agents (opens in new tab) - Measures the gap between visible validation tests and held-out tests and finds it growing by 28 percentage points per tenfold increase in code size.
- Building to the Test: Coding Agents Deliver What You Check, Not What You Requested (opens in new tab) - Names the failure mode where an agent satisfies the checked signal while the requested behaviour stays unverified.
- Test Driven Development, Martin Fowler (opens in new tab) - The red-green-refactor cycle that the five-step task shape hard-codes into every task.
Related posts
How GitHub's Spec Kit turns loose AI code generation into structured, maintainable output through a four-phase specify-plan-tasks-implement loop.
ci-cd · ai-tools · code-quality +4
A practical repo layout that keeps Claude Code, Codex, Copilot, Cursor, and OpenCode reading the same rules, with honest notes on where portability breaks.
ai-tools · claude-code · github-copilot +3
A framework for six levels of AI assistance in software, from code review to vibe coding, with guidance on when to dial AI help up or down.
ai-tools · code-quality · productivity +4
When a coding agent underperforms, the reflex is a stronger model. On bounded tasks the harness moves the score at least as much; a rule for which lever to pull.
ai-agents · ai-tools · llm +3
Devcontainers, Codespaces and AWS Lambda MicroVMs as homes for a coding agent: what each rung adds, what it costs, and when moving the agent off the laptop pays off.
lambda · claude-code · ai-tools +5