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: fix perf feat refactor chore docs ci build style test breaking analysis
  • 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 --force rewrites 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 generated sdd.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 list hides cancelled by default (--all shows them); spec validate and release diff ignore them.
  • Deprecated status names are mapped on read: planned/pending-approval/approved/pending-verification/in-progress/ready → draft, implemented → done.
  • spec submit / spec finalize are 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)