Writing PRDs
The PRD grammar, criteria and requirements, the lints that check them, and the scaffold marker.
zurdo v1.36.0
A Zurdo PRD is markdown with a strict grammar. If it parses, Zurdo can run every acceptance criterion itself.
Caution
Important If you remember three things: use a real em-dash (—, U+2014) in task headings, leave no blank line under the heading, and give every criterion at least one hint.
The grammar in one example
- Title. The file starts with # PRD: and a name.
- Task heading. ## Task: task-auth, an em-dash, then the title. Ids match ^task-[a-z0-9-]+$ and are unique in the PRD.
- Metadata. Directly under the heading, no blank lines. Effort and Depends-on are required.
- Requirements. Optional, and before Description when present.
- Description. Required. Sent verbatim to the agent.
- Acceptance Criteria. Run by Zurdo, never by the agent. Each needs at least one hint.
# PRD: <free-form title>
## Task: task-1 — <task title>
**Effort**: medium
**Depends-on**: []
**Max-Attempts**: 5
**Skills**: rust-style
**Agent-timeout**: 30m
**Frozen**: docs/adr/*.md
**Coverage**: 80% — reused from the service's existing suite
### Requirements
- req-tests-green: The workspace test suite passes.
### Description
Free-form prose. Passed verbatim to the executor agent.
### Acceptance Criteria
- [ ] cargo tests pass [shell: cargo test --workspace] [proves:req-tests-green]
- [ ] /health returns 200 [http: GET http://localhost:8080/health -> 200]
- [ ] design review signed off [manual]
The rules that trip people up
$ zurdo validate bad/hyphen.md
bad/hyphen.md:3: task heading uses hyphen-minus (U+002D) where an em-dash (U+2014) is required
$ zurdo validate bad/blank-line.md
bad/blank-line.md:3: blank line between task heading and metadata block
$ zurdo validate bad/unknown-key.md
bad/unknown-key.md:6: unknown metadata key `Priority`
$ zurdo validate bad/no-hint.md
bad/no-hint.md:11: criterion has no hints in task `task-1`
| Rule | Good | Bad |
|---|---|---|
| The heading separator is an em-dash | ## Task: task-1 — Greet | task-1 - Greet, task-1 – Greet |
| Metadata sits directly under the heading | **Effort**: low on the next line | a blank line between them |
| Only the eight metadata keys below | **Effort**: low | **Priority**: high |
| Every criterion has at least one hint | - [ ] tests pass [shell: cargo test] | - [ ] the greeter works |
Effort is a key of the active [effort_map.<provider>] | low, medium, high in the default config | a level your map doesn't define |
On macOS, type the em-dash with Option+Shift+-. Turn off any editor setting that swaps dashes in .md files.
| Key | Required? | Notes |
|---|---|---|
Effort | yes | A key in the active executor's effort map. |
Depends-on | yes | [task-1, task-2], or []. Satisfied once each dependency is passed or passed-pending-review. |
Max-Attempts | no | A positive integer. |
Agent-timeout | no | 30s, 15m, 1h. A bare integer fails. |
Skills | no | Bare skill names. Zurdo never installs them; it warns at pre-flight when one is missing. |
Frozen | no | Globs the agent must not modify, joined with [verification] protected_paths. Touching one fails the iteration. |
Category | no | Groups tasks in reports. Older PRDs use it; new ones don't need it. |
Coverage | no | <N>% — <reason> overrides the coverage gate's threshold for this task; exempt — <reason> skips the gate entirely. The reason is mandatory; a malformed value or a repeated key is a parse error. |
Criteria and requirements
A criterion is a checkbox line ending in one or more hints. Zurdo never reads or writes the checkbox; the hints are what run. Several hints on one line must all pass.
### Requirements lets you say what a task must achieve, and [proves:<id>] marks which criterion shows it:
### Requirements
- req-auth-1: Tokens expire after 24 hours
- req-auth-2: Expired tokens are rejected with HTTP 401
### Acceptance Criteria
- [ ] token expiry is enforced [shell: cargo test auth::token_expiry] [proves:req-auth-1]
- [ ] expired token rejected [http: GET http://localhost:8080/protected -> 401] [proves:req-auth-2]
- Ids match
^req-[a-z0-9-]+$. A[proves:]naming an undeclared id is an error. - A declared requirement nothing proves is an
uncovered-requirementwarning. [proves:]is not a hint. A criterion carrying only[proves:req-a]still fails with criterion has no hints.
Pre-authored tests
If the agent writes both the feature and its test, it controls both sides of the check. Write the test first, commit it with the PRD, and mark it ignored so CI stays green:
### Description
The tests marked `#[ignore = "pre-authored: task-03"]` in `tests/preauthored_task_03.rs`
are evidence. Do not delete them or weaken their assertions. The only edit this task
may make to that file is removing the `#[ignore …]` attribute.
### Acceptance Criteria
- [ ] login rejects a missing password [shell: cargo test -- --include-ignored login_rejects_missing_password]
- [ ] no pre-authored test is still ignored [no-grep: #\x5bignore in tests/preauthored_task_03.rs]
- Use
--include-ignored, never--ignored: once the attribute is gone,--ignoredmatches nothing and the criterion fails as a zero-test run. - Stub every function the test calls (
todo!()), so it compiles. - Run the hint yourself before committing. It must fail on an assertion, not on a compile error.
- In Go, tag the file
//go:build preauthoredand pass-count=1.
Validate early, analyze before you spend
zurdo validate prds/feature.md # free, instant: grammar, dependency graph, lints
zurdo validate --strict prds/feature.md # promotable lint warnings become errors (exit 2)
zurdo analyze prds/feature.md # adds lesson obligations and an LLM critique
prds/docs.md:15: warning: task `task-1` shell hint `true` is a no-op and proves nothing about its criterion
prds/docs.md:16: warning: task `task-1` grep pattern `hello` already matches `main.rs` in the current working tree — the criterion may not prove the change
prds/docs.md:9: warning: task `task-1` requirement `req-ci` is not proven by any criterion
OK
Zurdo has twenty-two lint families. zurdo validate emits twenty-one; unaddressed-lesson comes from zurdo analyze only. --strict promotes nine of the twenty-two to errors:
Promoted by --strict | Warn only |
|---|---|
grep-target, vacuous-shell, grep-tautology, frozen-overlap, doc-echo, uncovered-requirement, unreviewed-scaffold, hint-path, shell-tautology | skill-resolution, discarded-evidence, cached-verification, unaddressed-lesson, frozen-suggest, http-missing-server, stale-artifact, already-holds, no-grep-vacuous, existence-only, http-race, grep-should-be-symbol, assertion-free-test |
What each family means for your criteria is on the Hints reference. How each one decides is on Internals.
The scaffold marker
zurdo scaffold drafts a PRD, its .trail.md, and its pre-authored tests from a design document, or from one --section of it. The draft opens with this line:
<!-- zurdo:scaffold unreviewed — run the zurdo-prd-author skill before zurdo run -->
While it's there, validate reports unreviewed-scaffold, and validate --strict exits 2. Run the zurdo-prd-author skill on the draft: it reviews the PRD and removes the marker only when it reaches ✓ READY TO RUN. Don't delete the line by hand. The walkthrough is on Your first PRD.
Authoring with the bundled skills
zurdo-prd-authordrafts criteria first, derives tasks from them, and pressure-tests every hint. It writes a<prd>.trail.mdrecording why, and any new lessons.zurdo-design-authorcomes first when the work is too big for one PRD.zurdo-prd-reviewcompares a run's diff with what each task meant, and turns a gap into a follow-up PRD.
zurdo init installs the eight bundled skills.