Plan before you prompt

Structuring a Repo for AI Coding Tools

This post as MarkdownAll copies

You can one-shot a trivial application (the so-called "build me a web application and don't make mistakes" prompt). Anything more complex, work that has to be maintained and built upon, needs a more structured workflow.

The same discipline as in the days before AI coding tools applies: define clear product requirements and acceptance criteria, think through the technical design, write down the architecture, break the work into smaller tasks, and implement incrementally with checks and reviews. This post covers that approach: four documents for the architecture, one ADR, a plan, and the tasks, used to steer the coding agent.

The four documents

Each document has a singular purpose:

Document The question it answers Lifespan
ARCHITECTURE.md What does the system look like right now? Permanent, updated in every branch that changes a boundary
ADR Why did we make this one hard-to-reverse decision, and why did the alternatives lose? Permanent, immutable once accepted
PLAN.md How will this specific piece of work happen, and what is out of scope? Lives while the branch is open
TASKS.md What are the checkable steps, in order, with a verify command for each? Lives while the branch is open

The architecture document describes what exists today. Not where the system is headed. Not what was considered and rejected. The moment it mixes current state with future plans, it stops being a reference. Keep the diagram as text (PlantUML or Mermaid) so it lives in git and an agent can update it in the same commit as the code.

An ADR records one decision. The context states the constraints. The decision is one sentence. For example: "We will inject sleep and now into the client." Every serious alternative appears, including the obvious shortcut, with the reason it loses. Consequences include the cost. A Confirmation section lists checks a later reviewer or agent can run. Not every change needs an ADR. Write one when a change alters a contract, a service boundary, a lifecycle, or an owner, or when it locks in something hard to reverse. Bug fixes do not qualify.

The plan describes the work and its boundaries. The boundaries matter as much as the stack. A plan that only lists what to build accumulates scope as the agent reads it, because "why not add auth while I'm here" is a natural inference for a model. Writing down what is out of scope closes that door.

A task is verifiable or it is not a task. "Improve API handling" is a mood. This is a task:

text
- [ ] T021 Parse rel="next" from the GitHub Link header — src/rate_limited_cli/pagination.py — verify: uv run pytest tests/test_pagination.py

You can read that checklist after the agent runs and know whether the work happened without opening a single source file. Add Do not … tasks where the work could drift past an ADR, and make the verify step the ADR's own Confirmation check. The rule is then enforced by a test rather than by memory.

Acceptance criteria round out the set. Define "done" before the agent starts: a 404 prints "user not found," not a traceback; three mocked pages produce exactly three requests; tests pass with no network. The agent then checks against a list you wrote while you were thinking clearly, not one it invents while trying to make tests green.

Where everything lives

The layout separates the permanent record from the per-feature working set. Permanent documents sit at the top of docs/. Everything tied to one feature sits in one folder named after that feature.

text
.
├── AGENTS.md                      # the one file every agent reads first
├── CLAUDE.md                      # one line: "Read AGENTS.md"
├── .cursor/rules/                 # Cursor rules, scoped by glob
├── .claude/commands/plan.md       # /plan: turn an accepted ADR into a plan and tasks
├── .github/PULL_REQUEST_TEMPLATE.md
├── docs/
│   ├── ARCHITECTURE.md            # the system as it exists today
│   ├── adr/
│   │   ├── README.md              # index, status legend, when an ADR is required
│   │   └── 2026-10-30-split-github-client-into-collaborators.md
│   ├── features/
│   │   ├── v1-repos-command/      # PLAN.md, TASKS.md, ACCEPTANCE.md, PROMPTS.md
│   │   └── resilient-github-client/
│   └── templates/                 # ADR, PLAN, TASKS, ACCEPTANCE, PROMPTS sources
├── src/
└── tests/

Three choices in that tree are deliberate.

ADRs use a date-slug filename, not a sequence number. 2026-10-30-split-github-client-into-collaborators.md is the ADR's ID. Two engineers on two branches can each add an ADR without a merge conflict over who gets 0007. The filename sorts chronologically and reads as a sentence in a PR title.

Each feature owns one folder. docs/features/<slug>/ holds the plan, the task list, the acceptance criteria, and the bounded prompts for that feature. When the branch merges, the folder stays as a record of how the change happened. Nobody reads it by default, and that is fine. The plan links to its ADRs; the ADRs never link back to a plan, because an ADR outlives any one plan.

The architecture doc is singular and lives at the top. There is one current system, so there is one description of it. Every feature branch that moves a boundary updates it in the same PR, and the PR template asks for that update by name.

The product spec sits outside the repo, in whatever tracker the team uses. The ADR and plan link to it. Scope, deferred items, and the success measure are copied from the spec into the plan, never inferred.

From decision to merge

mermaid
flowchart LR
    A[Spec in the tracker] --> B[ADR opened: Proposed]
    B --> C[Reviewed in a PR]
    C --> D{{Status: Accepted}}
    D --> E["/plan: plan and tasks"]
    E --> F[One phase per prompt]
    F --> G[Review the diff]
    G -->|next phase| F
    G --> H[Merge, with ARCHITECTURE.md updated]

The decision is reviewed before any code exists. The code is reviewed one phase at a time.

  1. The spec names the problem, the v1 scope, the deferred list, and one success measure. It lives in the tracker, not the repo.
  2. The engineer opens a PR that adds one ADR with Status: Proposed. The PR contains no code.
  3. A reviewer accepts or rejects from the file alone. On rejection the reason goes under Decision and the status becomes Rejected. The file stays as a record.
  4. On agreement the status becomes Accepted. Open questions must be empty first.
  5. /plan docs/adr/<id>.md reads the accepted ADR and the spec, inspects the anchors in the code, and drafts the plan and the tasks. It reports gaps instead of filling them. A gap reported is useful. A gap filled with a guess is an unreviewed architectural decision.
  6. Each prompt names one phase: which tasks to implement, what not to implement, which tests to run, and where to stop. Phase 0 is foundation and blocks the rest. Each later phase is one user-visible slice with its own independent check.
  7. You review the diff at the phase boundary. A diff that spans five decisions is a diff whose reasoning you have lost.
  8. The PR that merges the feature also updates ARCHITECTURE.md. The PR template asks for it.

The gate is step 4. Nothing downstream begins until a human has accepted the decision in writing.

A worked example: rate-limited-cli

To make this concrete, the template repo builds a small Python CLI twice. The first pass is v1. The second is a feature branch that adds the parts v1 deliberately left out.

Bash
rate-limited-cli repos octocat

The command fetches every public repository for a GitHub user, follows pagination, and prints a summary: repo count, stars, forks, and remaining API quota. GitHub is a good sample API because its edge cases are real. Unauthenticated requests are capped at 60 an hour. Rate limit state arrives in x-ratelimit-* headers. List endpoints paginate through a Link header, and GitHub tells you to follow rel="next" rather than increment a page number yourself.

v1 needs no ADR. It is the first version of a small tool, and nothing in it is hard to reverse. It needs a plan, a task list, and acceptance criteria. The plan fixes the stack (Python 3.12, uv, Typer, httpx, pytest with respx, Ruff) and the surface (repos USER --limit --json). It also says what v1 excludes: no authentication, no retry engine, no persistent cache. The client does the minimum correctly. It follows Link: rel="next" until the link disappears and stops early when --limit is reached.

The second pass is where ADRs earn their keep. The feature branch adds token authentication, rate-limit handling, retry with backoff, ETag caching, and clearer diagnostics. The design question has changed. It is no longer "how do I call GitHub." It is how five concerns interact without turning GitHubClient into one large method that does all of them badly. That is a hard-to-reverse decision, so it gets an ADR.

The ADR records the priority order for rate limits. If Retry-After is present, honor it. If the primary limit is exhausted, wait until reset, but only up to a bound. Otherwise treat it as a secondary limit and back off exponentially. It records the split of GitHubClient into four collaborators: AuthConfig, RateLimitPolicy, RetryPolicy, and CacheStore. And it records what lost. Inlining everything in the client is untestable. Always sleeping until reset means a CLI hangs for 40 minutes because that is when the quota resets. A --token flag puts a secret in shell history, so GITHUB_TOKEN from the environment wins. A plugin framework is not needed, so it is not built.

One detail from the ADR is worth keeping concrete, because it decides whether retry code is testable or miserable. Do not call time.sleep and time.time directly. Inject them.

Python
class GitHubClient:
    def __init__(self, *, sleep=time.sleep, now=time.time):
        ...

A test passes sleep=delays.append and a fixed now, then asserts delays == [5] after a mocked 429 with Retry-After: 5. The suite never waits five seconds. The ADR's Confirmation section turns this into a check an agent can run: grep the client for time.sleep( and expect zero matches. A Do not … task in TASKS.md carries that same check as its verify step.

ARCHITECTURE.md changes in the same branch. The diagram that showed CLI -> GitHubClient -> GitHub now shows the four collaborators. The ADR says why the split happened. The architecture doc says what the system looks like now. If the same sentence appears in both, one of them is redundant, and it is usually the architecture doc trying to be a decision log.

Wiring the agent to the documents

The documents only help if the agent reads them. Every tool has its own instruction file, and the fastest way to drift is to maintain four copies of the same rules. Keep one source and point everything else at it.

File Read by What belongs in it
AGENTS.md Codex, OpenCode, Cursor, and anything that follows the open convention The project map, the commands to run, the design rules, and the order in which to read docs/
CLAUDE.md Claude Code One line: read AGENTS.md. Nothing else, so the two never disagree
.cursor/rules/*.mdc Cursor Short rules scoped by glob: src/** says where each concern lives; docs/adr/** says ADRs are immutable once accepted
.claude/commands/plan.md Claude Code as /plan The procedure that turns an accepted ADR into a plan and tasks, with its stop conditions
docs/features/<slug>/PROMPTS.md You, pasting one at a time The bounded prompts for each phase
.github/PULL_REQUEST_TEMPLATE.md The reviewer Which ADR this PR implements, which tasks it closes, whether ARCHITECTURE.md changed

Three rules keep the instruction files useful.

Tell the agent what to read, in order. AGENTS.md lists docs/ARCHITECTURE.md, then the ADRs the current branch cites, then the feature folder. An agent that reads the plan before the architecture doc will propose a plan for a system that does not exist.

Tell the agent when to stop. Every bounded prompt ends with "Update only completed task checkboxes. Stop." The /plan command stops if the ADR is not Accepted, if the spec cannot be opened, or if a plan file already exists. A procedure without a stop condition becomes a procedure that runs until something breaks.

Treat TASKS.md as the agent's memory. Agents lose context between sessions. A task list with checkboxes and verify commands lets the next session pick up one unfinished task, run its verify step, and mark it done. That is the same pattern Anthropic describes for long-running agents: an initializer turns the request into a feature list and a progress file, and each later session works one item and leaves the file updated for the next.

What survives the merge

The plan and the task list are useful while the branch is open and mostly useless six months later. Nobody reviewing the code in a year needs docs/features/resilient-github-client/TASKS.md to understand why caching works the way it does. They need the README, the current ARCHITECTURE.md, and the ADR. The feature folder stays in the repo as a record of how the change happened. It is not the primary documentation.

This is not about producing more documentation. It is about making the design decisions before the agent makes them for you by default. Writing the ADR first is slower than typing "add retry logic with backoff" into a prompt. It is also the difference between reviewing a plan you agree with and reverse-engineering a decision from a diff.

The template repo, with the ADR and plan templates, the /plan command, the agent instruction files, and both passes of the sample CLI, is vivekchopra/rate-limited-cli. Clone it, delete the sample feature, and keep the structure.

References

Decision records

Spec, plan, and tasks

  • GitHub Spec Kit. Constitution, spec, plan, tasks, implement. Source of the T001 numbering, [P] parallel markers, phased tasks, and a cross-artifact consistency check before implementation.
  • Spec Kit issue #1934. Proposes explicit (depends on T001) suffixes, because [P] alone does not tell an agent which tasks are safe to run together.
  • AWS Kiro: Specs. Requirements, design, and tasks files. Each task lists the requirement IDs it covers, and independent tasks run in dependency-ordered waves.

Agents working across sessions

Sample project