Spec-Driven Development
Spec-Driven Development with Polycrate: plan, implement, document and version changes. Canonical flow for workspace and block releases, exact gate table, agent integration.
Spec-Driven Development (SDD) is the process by which changes to Polycrate workspaces and blocks are planned, implemented, documented and versioned. Polycrate ships a standardized .specs/ layout, a machine-readable spec format with an ID-based lifecycle, and the CLI commands polycrate spec * and polycrate release *. SDD is designed for working with coding agents (Cursor, Claude Code, Codex, …) but works just as well without them.
This page describes exactly what the CLI checks — nothing more. The single source of truth is polycrate spec manual (section CANONICAL FLOW); this page, the generated .cursor/rules/sdd.mdc and Dev and Deploy mirror it.
Why Spec-Driven Development?
| Question | SDD answer |
|---|---|
| Why was something changed? | The spec archives context, analysis and solution — permanently, next to the code |
| What is the current state? | Live scan of .specs/: folder location + frontmatter are the state |
| How does a change reach a release? | draft → done → release create → release update → release finalize |
| What is open? | Backlog in .specs/.next/; pending decisions in ## Open Questions |
| What must an agent not decide? | Anything listed as an Open Question — the CLI refuses done while any remain |
| What does the target architecture look like? | Optional .specs/ARCHITECTURE.md, freely editable |
The canonical flow
There is exactly one flow. Everything else on this page explains its building blocks.
A. Workspace change (spec → release)
W=/abs/path/to/workspace # -w must be absolute — or omit it when cwd is the workspace
# 1. Plan — one problem or feature per spec
polycrate spec create --name "Rotate hub token" --type feat -w $W
# → .specs/.next/<id>-<slug>.md, status: draft (IDs start at 1)
# 2. Write — edit section bodies directly (H3+ only) or via flag
polycrate spec update <id> --section-file solution=/tmp/.polycrate/spec-sections/spec-<id>-solution.md -w $W
# 3. Check structure — drift blocks every status and release operation
polycrate spec lint <id> -w $W # report
polycrate spec format <id> -w $W # repair, bodies are preserved
# 4. Implement. Informative checks (never block):
polycrate spec verify <id> -w $W # acceptance checkboxes + @path:LINE anchors
polycrate spec review <id> --no-pager -w $W
# 5. Close — Open Questions MUST be empty (resolve into Solution/Context, do not delete)
polycrate spec update <id> --set status=done -w $W
# 6. Release — specs are optional; a pure version bump with a release note is valid
polycrate release create 1.4.0 --spec <id> -w $W
polycrate release update 1.4.0 --set type=feat --set message="Hub token rotation" \
--changelog-item "Feature: hub tokens rotate automatically" \
--note-section highlights="- Hub token rotation without downtime" -w $W
polycrate release status 1.4.0 --no-pager -w $W # shows what finalize will check
# 7. Finalize — sets workspace.poly version, runs pre_actions/post_actions. No git.
polycrate release finalize 1.4.0 -w $W
B. Block change (blocks have no specs)
Blocks carry no .specs/ directory. A block release consists of the CHANGELOG.poly entry, the block.poly version and the registry tag.
- Logic change in a block → one workspace spec in the repository that hosts the block, labelled
block:<name>. Its lifecycle is A.1–A.5. - Pure version bump (dependency, image tag, no behaviour change) → no spec; the changelog entry documents it.
# 0. Only for logic changes: spec in the host workspace, set the label
polycrate spec update <id> --add-label block:hardening -w $W
# 1. Changelog entry (idempotent; creates the entry if missing)
polycrate release --block hardening update 0.5.6 --set type=fix \
--set message="Configurable OpenSSH PerSourcePenalties" \
--changelog-item "Fix: exempt list for PerSourcePenalties" \
[--set app_version=A.B.C] -w $W
# 2. Finalize — sets block.poly version, validates, pushes to the registry (idempotent)
polycrate release block-finalize hardening 0.5.6 [--app-version A.B.C] [--no-push] -w $W
# 3. Consume downstream — rewrites from: on every instance of this block in the workspace
polycrate block pull cargo.ayedo.cloud/ayedo/linux/hardening:0.5.6 -w /abs/path/to/downstream-workspace
polycrate run hardening install -w /abs/path/to/downstream-workspace
release --block <name> create, spec --block, release --block update --spec and --note-section* with --block no longer exist and abort with a hint pointing to this flow.
C. Gates — what actually fails (exit ≠ 0)
| Operation | Hard fail when | Fix |
|---|---|---|
| every command | -w is not absolute |
pass the full path or omit -w |
spec update --set status=done |
structural drift (headings, order, frontmatter) | spec format <id> |
spec update --set status=done |
## Open Questions not empty |
resolve into Solution/Context |
spec update --section* |
H1/H2 inside a section body | use H3+ |
release create/update --spec <id> |
spec is not done or cancelled |
status=done first |
release finalize (workspace) |
drift in an assigned spec | spec format |
release finalize (workspace) |
Open Questions in an assigned spec | resolve, then status=done |
release finalize (workspace) |
CHANGELOG.poly entry without message |
release update --set message= |
release finalize (workspace) |
RELEASENOTE.md highlights empty |
release update --note-section highlights= |
release block-finalize |
changelog entry missing, message/description empty |
release --block <name> update |
release block-finalize |
app_version empty although block.poly tracks one |
--app-version or --set app_version= |
release delete / spec delete |
interactive confirmation declined | --force skips the prompt |
--force is an audited emergency brake (--force-reason required, logged to .specs/.force-audit.log): on status=done it bypasses drift and Open Questions; on release finalize it bypasses only highlights — message, drift and Open Questions of assigned specs stay hard.
Not gates (warning or silent): results of spec verify, a missing spec review, unmet needs:, 0 specs on a release, the git state (dirty tree, existing tag, unpushed commits).
D. Git is not a process step
Whether a workspace or block can be released does not depend on the state of any git repository. release finalize creates no tag and no commit; nothing checks the working tree. Released means: CHANGELOG.poly entry + version file (workspace.poly/block.poly), or the registry tag for blocks. Agents never run git commit/tag/push/stash/reset or polycrate workspace sync as a spec or release step. Whoever wants git history commits themselves — see the Git integration page (German: /docs/polycrate/git/).
Concepts
Specs
A spec is a Markdown document with YAML frontmatter that accompanies a change from idea to implementation:
- ID — integer, stable, never reused;
polycrate spec inspect <id>finds it regardless of location - Status —
draft,done,cancelled - Type — Conventional Commits:
fixperffeatrefactorchoredocscibuildstyletestbreakinganalysis - Sections — fixed schema (see below)
- Labels — free-form, e.g.
block:<name> - needs / relates-to — dependencies (warning on
done, not a gate) and topical links
Folder location is the state
There is no index.yml. Specs and releases are read live from the filesystem:
| Path | Meaning |
|---|---|
.specs/.next/<id>-<slug>.md |
Backlog — in progress, not assigned to any release |
.specs/<version>/<id>-<slug>.md |
Assigned to release <version> |
.specs/<version>/RELEASENOTE.md |
Release note (scaffolded by release create) |
my-workspace/
├── .specs/
│ ├── ARCHITECTURE.md # reference architecture — freely editable
│ ├── .rules.md # project rules for agents — written by spec init, extendable per repo
│ ├── .invariants.md # hard, checkable repo rules — one per line
│ ├── .next/
│ │ ├── 7-rotate-hub-token.md
│ │ └── 8-login-timeout.md
│ └── 1.4.0/
│ ├── RELEASENOTE.md
│ └── 5-s3-bucket-provisioning.md
├── .cursor/rules/sdd.mdc # generated: polycrate spec rules generate
├── blocks/<name>/ # block.poly + CHANGELOG.poly — no .specs/
├── workspace.poly
└── CHANGELOG.poly
What may be edited directly
| Element | Allowed | Tool |
|---|---|---|
Section bodies (text between two ## headings) |
yes | editor / agent file tools |
.specs/ARCHITECTURE.md |
yes | editor |
| YAML frontmatter (status, type, labels, …) | no | polycrate spec update --set |
## section headings, order |
no | polycrate spec format <id> |
.specs/<v>/RELEASENOTE.md |
no | polycrate release update --note-section |
CHANGELOG.poly (workspace and block) |
no | polycrate release update / release --block update |
version in workspace.poly / block.poly, app_version |
no | release finalize / release block-finalize |
After direct body edits run polycrate spec lint <id>; spec format repairs drift and preserves content.
Section schema
| Section | Content | Gate |
|---|---|---|
context |
Background, requirements, analysis — for fix/perf/analysis: symptom, analysis, root cause |
— |
solution |
Concrete plan with @path/file.ext:LINE anchors; may be empty only for analysis |
— |
affected-files |
One path per line | — |
acceptance-criteria |
- [x] <condition> - verify: @path:LINE — spec verify checks boxes and anchors |
informative |
migration |
Upgrade steps; required for type=breaking |
spec validate error |
open-questions |
Pending decisions | empty before status=done |
review |
Self-review: Finding N [minor\|major\|blocker] @path:LINE: … - resolved: yes\|no |
informative |
Inside sections only H3 (###) and deeper are allowed; H1/H2 are rejected on write because H2 is the section boundary.
.specs/.rules.md and .specs/.invariants.md
.rules.md— process rules in prose.polycrate spec init --forcerewrites the current default (specs are untouched); append repo-local additions as their own##section and re-add them after a rewrite..invariants.md— one hard, checkable rule per line (max. 25). Comes first in the generatedsdd.mdc: violations are bugs, not matters of convention.
# Repo invariants — my-project
- LANG: all user-facing output in English
- DB: use ORM only; no raw SQL
- SDD: in .specs/ only section bodies and ARCHITECTURE.md are hand-editable
- SDD: git is not a process step — never commit/tag/stash/reset/sync as part of spec or release work
- SDD: blocks have no .specs/ — block-related specs live in the workspace with label block:<name>
Reference architecture (.specs/ARCHITECTURE.md)
Not a spec document, not scanned, freely editable. Recommended as a compact, living architecture document (headings, tables, code pointers). polycrate spec arch list|search <query>|show <slug> returns individual sections on demand — agents read only the relevant ones before planning.
Spec lifecycle
draft ◄──────────────► done gates: no drift, Open Questions empty
└──────────┬──────────┘
cancelled any time, no gate (set cancellation_reason)
polycrate spec update 7 --set status=done
polycrate spec update 7 --set status=draft # reopen
polycrate spec update 7 --set status=cancelled --set cancellation_reason="Superseded by spec 9"
spec listhidescancelledby default (--allshows them);spec validateandrelease diffignore them.- Deprecated status names are mapped on read:
planned/pending-approval/approved/pending-verification/in-progress/ready→draft,implemented→done. spec submit/spec finalizeare deprecated aliases for--set status=done.
Open Questions — the one workflow gate
An open question is a missing decision. So that an agent does not silently make it “during implementation”, the CLI refuses status=done while ## Open Questions contains text (HTML comments count as empty). Answered questions belong in solution/context — the section is resolved, not emptied.
● spec 7: Open Questions is not empty — resolve or clear the section before status=done
Dependencies and links
polycrate spec link 5 --needs 3 --needs 4 # 5 requires 3 and 4 — warning on done, not a gate
polycrate spec link 5 --relates-to 8 # topical link, no effect on the lifecycle
polycrate spec update 5 --remove-needs 3
polycrate spec list --blocked # specs with non-done needs
Self-references and cycles are rejected.
Informative checks
| Command | Checks | Effect |
|---|---|---|
spec lint <id> |
Structural drift (blocking on status/release) and grounding hints (anchors in Context/Solution, existing Affected Files) | drift → error; grounding → warning |
spec review <id> |
Stamps reviewed_at; status=done warns when the stamp is missing or older than the file |
warning |
spec verify <id> |
Acceptance checkboxes and @path:LINE anchors; --format json, --with-agent (prompt for a verifier subagent) |
warning; status=done prints the same table on stdout |
| needs check | non-done dependencies on status=done |
warning |
Quick capture
polycrate spec note "S3 upload fails silently on 403" --type fix --label s3
# → draft spec created immediately, ID returned, current task continues
Overview
polycrate spec list # compact table / TUI
polycrate spec list --open # not done, not cancelled
polycrate spec list --version .next --status done # backlog: done but not released
polycrate spec list --label block:hardening
polycrate spec list --format json # field is "version" (empty = backlog), not "release"
polycrate spec inspect 7 --section solution
Release lifecycle
Workspace release
polycrate release diff 1.4.0 # preview: which specs are done and unassigned?
polycrate release create 1.4.0 --spec 7 --spec 8 # moves the files to .specs/1.4.0/, scaffolds RELEASENOTE.md
polycrate release create 1.4.0 --spec-status done # bulk: all done specs from .next/
polycrate release create 1.4.1 # without specs — pure bump
polycrate release update 1.4.0 --set type=feat --set message="Summary" \
--changelog-item "Feature: …" --changelog-item "Fix: …" \
--set docs_url=https://ayedo.de/docs/polycrate/releases/
polycrate release update 1.4.0 --note-section highlights="- …" --note-section-file changes=/tmp/.polycrate/spec-sections/changes.md
polycrate release status 1.4.0 --no-pager # guards, warnings, checklist
polycrate release finalize 1.4.0 # set version, pre_actions → post_actions
release finalize sets workspace.poly → version, runs the actions configured in workspace.poly under config.release.pre_actions / post_actions (e.g. build, image push, hub upload) and writes nothing to git. If a pre_action fails, finalize stops — even with --force.
Release notes (RELEASENOTE.md)
| Section | Required for finalize |
|---|---|
highlights |
yes |
changes, fixes, migration |
no |
polycrate release note status 1.4.0
polycrate release note inspect 1.4.0 --section highlights
Again: only H3 and deeper inside sections.
Block release
See flow B. Two versions in block.poly, both CLI-only:
| Field | Means | Set by |
|---|---|---|
version |
Block logic (playbooks, templates, actions) | release block-finalize |
app_version |
Upstream application the block installs | --app-version or release --block update --set app_version= |
Re-running block-finalize on the same state is a safe no-op: if the tag exists in the registry, the push is skipped and the state is confirmed (--force pushes again).
polycrate release --block hardening status 0.5.6 --no-pager # changelog, version file, registry tag
Validation and recovery
polycrate spec validate # all specs: frontmatter, sections, needs, migration on breaking
polycrate release validate 1.4.0 # specs in the directory done, changelog entry, version file
polycrate spec format <id> # repair drift
polycrate spec force-audit # all --force usages
Legacy repos with index.yml and the 5-status lifecycle: run polycrate spec index migrate once, then polycrate spec lifecycle migrate-v2.
Agent integration
Polycrate generates exactly one agent file: .cursor/rules/sdd.mdc. It contains the invariants, the absolute rules, the canonical flow with the gate list, the key commands and the content of .specs/.rules.md. AGENTS.md, CLAUDE.md or other IDE files are not written.
polycrate spec init -w $W # creates .specs/ and .specs/.rules.md (once)
polycrate spec init --force -w $W # rewrites only .rules.md to the current default
polycrate spec rules generate --force -w $W # writes .cursor/rules/sdd.mdc
polycrate spec rules show --no-pager -w $W # shows .specs/.rules.md
Run both after every CLI upgrade in every repo that has .specs/. Repo-local process rules belong in .specs/.rules.md, checkable conventions in .specs/.invariants.md; domain coding standards stay in their own .cursor/rules/*.mdc. For IDEs without Cursor rules, polycrate spec manual --no-pager is the entry point.
Session start
polycrate spec manual --no-pager -w $W # once per session, not per turn
polycrate spec list --open --format tsv -w $W
polycrate spec arch list -w $W # then search/show for the topics touched
Agent discipline
| Rule | Content |
|---|---|
| Spec handoff | The chat reply is the handoff: spec identity, summary (why + what/how), Open Questions spelled out or “No open questions.”, next step. Do not assume the user opens .specs/. |
| Soft stop | Pause on open questions: do not implement, do not set status=done. The CLI backs this — done fails on non-empty Open Questions. |
| Do not invent | A missing fact is an Open Question in the spec and the chat. |
| Architecture before guessing | spec arch search/show for every topic touched, before planning or implementing. |
| Self-review | Findings with @path:LINE and [minor\|major\|blocker]; “all OK” does not count. |
| Staging files | For --section-file under /tmp/.polycrate/spec-sections/, never under .specs/. |
| Code comments | // Spec: polycrate spec inspect 7 — never reference the file path, it changes on release. |
Multi-agent
Four roles per spec, sequential: grounding (spec arch), author (spec create/update, spec lint), reviewer (spec review), verifier (spec verify --with-agent). Specs without open needs are independent and can be worked on in parallel; spec list --version .next shows [BLOCKED] for waiting specs.
Sandbox and runtime directory
polycrate spec * and polycrate release * start no container. In restricted environments:
polycrate spec list --runtime-dir /tmp/polycrate-run -w $W
All commands at a glance
polycrate spec
| Command | Description |
|---|---|
spec init [--force] |
Create .specs/ + .rules.md; --force rewrites only .rules.md |
spec create --name <name> [--type <t>] |
New spec (draft) |
spec note "<text>" [--type] [--label] |
Quick capture |
spec list [--open\|--status\|--label\|--type\|--blocked\|--all\|--version\|--format] |
Overview |
spec inspect <id> [--section <name>] |
Show a spec |
spec update <id> --set k=v |
Frontmatter: status, type, name, cancellation_reason, reviewed_at |
spec update <id> --section k="…" \| --section-file k=path \| --section-stdin k |
Write section bodies |
spec update <id> --add-label\|--remove-label\|--add-needs\|--remove-needs\|--add-relates-to |
Labels, dependencies |
spec update <id> --force --force-reason "…" |
Bypass the drift and Open Questions gates (audited) |
spec link <id> --needs <id> \| --relates-to <id> |
Alias for dependencies |
spec lint <id> / spec format <id> |
Report / repair drift |
spec review <id> |
Self-review, stamps reviewed_at |
spec verify <id> [--format json] [--with-agent] |
Check acceptance criteria |
spec arch list \| search <q> \| show <slug> |
Targeted reference-architecture reads |
spec validate |
Consistency of all specs |
spec force-audit |
Show the force log |
spec delete <id> [--force] |
Delete a spec |
spec manual [--no-pager] |
Full manual (SSOT) |
spec rules generate [--force] / spec rules show |
Write .cursor/rules/sdd.mdc / show .rules.md |
spec index migrate, spec lifecycle migrate-v2 |
Legacy migration |
polycrate release
| Command | Description |
|---|---|
release diff <version> |
Preview of assignable specs |
release create <version> [--spec <id>…] [--spec-status done] [--spec-type <t>] |
Create a workspace release; assigned specs must be done/cancelled |
release update <version> --set type= --set message= --set docs_url= --changelog-item "…" |
CHANGELOG.poly entry |
release update <version> --note-section k="…" \| --note-section-file k=path |
RELEASENOTE.md |
release update <version> --spec <id> |
Assign another spec |
release status <version> / release list / release validate <version> |
Read |
release finalize <version> [--force --force-reason] |
Finalize the workspace (gates above) |
release --block <name> update <version> --set type= --set message= --changelog-item [--set app_version=] |
Block changelog entry |
release block-finalize <name> <version> [--app-version] [--no-push] |
Finalize + push the block |
release --block <name> status <version> |
Read a block release |
release note create\|inspect\|status <version> |
Release note |
release unassign <version> <id> / release delete <version> |
Spec back to .next/ / remove the release |
See also
- Dev and Deploy — the flow as a process map including MCP ops
- Getting started
- German-only reference pages: MCP server (
/docs/polycrate/mcp/), CLI reference (/docs/polycrate/cli-referenz/), Blocks (/docs/polycrate/blocke/), Workspaces (/docs/polycrate/workspaces/), Git integration (/docs/polycrate/git/, optional, not part of the process)