π End-to-end tutorial β building Smart Parking#
A friendly, step-by-step walk through every command in
create-spec-driven-app. We build one real backend project β Smart
Parking β on top of the public demo pack
rsaglobaltech/parking-management-specops.
You do not need to understand the whole tool up front. Each step explains the concept first, then the command, then what you should see.
csdais the short name forcreate-spec-driven-app. If you have not installed it globally, replace everycsdain this guide withnpx create-spec-driven-app@latest.
Before anything else: the two places you will work#
This is the single most important idea in the tool, and the one that trips people up. There are two separate folders on your disk, and they are not the same thing:
βββββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ
β THE PACK REPO β β YOUR IMPLEMENTATION PROJECT β
β parking-management-specops/ β β smart-parking/ β
β β β β
β β’ pack.yaml (the domain model) β β β’ spec.md β
β β’ templates/ (Gherkin templates) β β β’ features/**/*.feature β
β β βββΆ β β’ docs/specs/traceability.md β
β Reusable knowledge, versioned β β β’ src/, test/ (the code YOU write)β
β with git tags (v0.1.0, v0.2.0β¦). β β β’ .specops.lock β the link β
β You only open this if you AUTHOR β β β
β packs. β β Where you spend 95% of your time. β
βββββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ
the SOURCE of specs the project that CONSUMES them- The pack repo is a library of domain knowledge. Think of it like a package on npm β you usually just consume a published version of it, you do not edit it.
- Your implementation project is the app you are building. It is a
normal git repo with your real source code.
initcreated it;expand/specops addcopied rendered specs into it; and.specops.lock(a file inside your project) remembers which pack and version it came from.
The rule of thumb: unless a step explicitly says "the pack repo", you
run the command from inside your implementation project
(smart-parking/). Every step below carries a π badge so you are never in
doubt.
| You run it from⦠| These commands |
|---|---|
π your implementation project (smart-parking/) | init* Β· validate Β· plan Β· done Β· specops add Β· specops diff Β· specops sync Β· specops remove Β· expand Β· harness run |
π¦ the pack repo (parking-management-specops/) | pack init Β· pack lint Β· pack lint --graph Β· pack infer Β· and "add a requirement as a pack author" |
* init is run from the parent directory β it creates the project
folder. After that, you cd into it and stay there.
Table of contents#
- Prerequisites
- Step 1 β Scaffold the project (
init) - Step 2 β Apply the domain pack (
specops add) - Step 3 β Validate the project (
validate) - Step 4 β See what is left to build (
plan) - Step 5 β Implement a requirement and close the loop (
done) - Step 6 β Add a NEW requirement (as a project consumer)
- Step 7 β Upgrade the pack version (
specops diff+specops sync) - Step 8 β Remove a pack (
specops remove) - Step 9 β Author your own pack (
pack initΒ·lintΒ·--graphΒ·infer) - Step 10 β Add a NEW requirement (as a pack author)
11Β½. Step 10Β½ β Change a requirement that already shipped (
change) - Step 11 β Automate delivery with the harness (
harness run) - Step 12 β Companion tooling (VS Code Β· MCP Β· agents)
- Command cheat-sheet
1. Prerequisites#
- Node.js β₯ 22 and git on your
PATH. - Network access the first time you run
specops addβ it clones the pack repo into a per-user cache (~/.cache/csda/packs/β¦). After that it is offline.
Optionally install the CLI globally so csda works everywhere:
npm install -g create-spec-driven-app@latest
csda --version # 0.2.0
csda --help # the eight commands of the daily loop
csda --help --all # the whole surface2. Step 1 β Scaffold the project (init)#
π Run from: the parent directory (e.g.
~/sandbox).initcreatessmart-parking/for you.
Concept#
init does not write any business logic. It creates an empty-but-valid
spec-driven project: the folders, the requirements document, and the
traceability matrix β the skeleton everything else fills in. It is the
git init of spec-driven development.
You describe the project in a small config file. It can be a YAML
mapping (.yaml / .yml) or the legacy KEY="value" format (.config);
init picks the parser from the file extension. We use YAML here.
Do it#
mkdir -p ~/sandbox && cd ~/sandbox
cat > smart-parking.yaml <<'EOF'
# Required
PROJECT_NAME: Smart Parking
PROJECT_SLUG: smart-parking
PROJECT_TYPE: backend # backend | frontend | mobile
DOMAIN: parking operations
STACK: Quarkus 3.x, Java 21, PostgreSQL, RESTEasy Reactive, Maven
API_STYLE: REST with DTO boundaries
TESTING: JUnit 5, Testcontainers, Cucumber
# Optional
LANG: en
MODULES: "" # e.g. auth,billing
EOF
csda init --config ./smart-parking.yaml --out .
cd smart-parking # β from here on, you stay inside the projectFlags: --force (overwrite an existing folder), --dry-run (print
what it would do, write nothing), --no-git (skip git init).
What you should see#
smart-parking/
βββ spec.md # the requirements document β prose
βββ AI_RULES.md # stack guardrails for AI agents
βββ README.md
βββ features/ # Gherkin scenarios will live here
βββ docs/specs/
βββ traceability.md # the requirement β code matrix β the spine
βββ adr/The scaffold ships with one placeholder requirement, so the project is already valid. Now we give it a real domain.
3. Step 2 β Apply the domain pack (specops add)#
π Run from: inside your project β
smart-parking/.
Concept#
A domain pack is reusable, versioned domain knowledge: requirements,
use cases, commands, aggregates, events, and the Gherkin scenarios that pin
them down. It lives in its own repo (here:
parking-management-specops), tagged with versions like v0.1.0.
specops add is the npm-install of spec-driven development. It:
- clones the pack repo (at the version you pin),
- renders the pack's templates into your project β this is the arrow
in the diagram at the top: knowledge flows from the pack repo into
smart-parking/, - writes a
.specops.lockfile in your project that remembers the repo, version, commit and variables β so you never retype them.
You are not editing the pack. You are pulling a snapshot of it into your project.
Do it#
csda specops add \
--pack-repo https://github.com/rsaglobaltech/parking-management-specops.git \
--pack-version v0.1.0 \
--pack backend \
--var PROJECT_NAME="Smart Parking" \
--var PROJECT_SLUG=smart-parking \
--var DOMAIN="parking operations"What you should see β all of this lands inside smart-parking/#
- New
.featurefiles underfeatures/(vehicle entry, capacity threshold, billing, overstay, receiptsβ¦). - New rows in
docs/specs/traceability.mdβ one per scenario. - Rich domain docs:
docs/specs/{use-cases,aggregates,commands,events,domain-model}.md. .specops.lockβ the link back to the pack. Records repo, version, resolved commit, and your--varvalues..specops/baseline/β¦β a verbatim copy of exactly what the pack rendered. This is the "known-good ancestor" that makes a laterspecops syncable to merge safely instead of clobbering your edits.
β Commit
.specops.lockand.specops/to git. A teammate who clones your project needs both forspecops syncto work.
Useful flags: --dry-run, --no-examples (skip files marked
seed: true), --cache-dir <path>, --pack-root <path> (point at a local
pack folder instead of a git URL β handy offline or for testing).
expandis the low-level version of this.specops addis the friendly wrapper;expanddoes the same rendering with more explicit flags and no lockfile bookkeeping. Usespecops addday to day.
4. Step 3 β Validate the project (validate)#
π Run from: inside your project β
smart-parking/.
Concept#
validate is your safety net. It checks that the project still hangs
together: required files and folders exist, there is at least one
.feature, every feature file is referenced in the traceability matrix,
status values are valid, and the Gherkin parses.
Do it#
csda validate .A clean run prints Validation passed and a feature count.
The TDD gate β --strict-tdd#
Plain validate checks structure. --strict-tdd adds the rule that the
matrix may not run ahead of reality:
csda validate . --strict-tddIt additionally fails when:
- a row has a
TBDtest artifact but a status pastDraftβ[TDD-1], - a row has a status but no Scenario ID β
[TDD-2], - a
REQ-NNNis mentioned inspec.mdbut has no row intraceability.mdβ[TDD-3].
Wire validate --strict-tdd into CI and a git pre-commit hook. It is the
gate that keeps specs and code honest.
5. Step 4 β See what is left to build (plan)#
π Run from: inside your project β
smart-parking/.
Concept#
After specops add, your project has a pile of scenarios but no code yet.
plan answers "what do I do next?" It reads traceability.md and looks
at the filesystem, then tells you, per requirement, what is missing.
Do it#
csda planπ Plan (5 requirements, 5 pending)
β οΈ Test missing (write the test first)
REQ-001 SCN-001
β feature: features/capacity/capacity_threshold.feature
Β· test: capacity_threshold.steps
Β· code: Occupancy monitor
β¦Each requirement lands in one bucket: NEEDS_FEATURE, NEEDS_EVERYTHING,
NEEDS_TEST, NEEDS_IMPLEMENTATION, NEEDS_STATUS_UPDATE, or DONE. A
β means the file exists; a Β· means it is still missing.
Machine-readable mode#
csda plan --format jsonEmits a stable JSON structure (summary, next_steps[],
requirements[]β¦). This is the task queue the harness consumes in
Step 11, and what an AI agent reads to know what to work on.
6. Step 5 β Implement a requirement and close the loop (done)#
π Run from: inside your project β
smart-parking/.
Concept#
Now you write actual code. The loop for every requirement is always the same four moves:
- Read the
.featurefileplanpointed you at β it is the executable spec, the source of truth for behaviour. - Write the test first, so it fails for the right reason (TDD).
- Write the production code until the test passes.
- Close the loop with
doneβ this updates the matrix.
Do it#
# After the test + code for REQ-001 exist and pass:
csda done REQ-001 --checkdone flips that requirement's Status cell in traceability.md to
Implemented. --check runs validate first and aborts on failure, so
the matrix can never claim something is done while the gates are red.
--strict uses validate --strict-tdd instead. --status <Status> targets
another terminal state (Verified, Released, β¦).
Re-run csda plan β REQ-001 is now under β
Done.
7. Step 6 β Add a NEW requirement (as a project consumer)#
π Run from: inside your project β
smart-parking/. You do not touch the pack repo here.
Concept#
The pack covers parking operations in general, but your project needs
something extra β say, "operators can reserve a parking spot in advance."
You add it locally, in your project. It is yours; it is not part of the
pack; and β importantly β it will survive every future specops sync,
because sync only reconciles files the pack owns.
The workflow is the same validate/plan/done loop you already know,
with two new files in front of it.
7.1 Describe it in spec.md#
Add a section to spec.md:
## REQ-101 β Reserve a parking spot in advance
An operator can reserve a specific spot for a future time window so a
known vehicle is guaranteed space on arrival.Use an ID range that will not collide with the pack. The pack uses
REQ-001β¦; keep your project-local requirements atREQ-101and up.
7.2 Write the Gherkin scenario first#
mkdir -p features/reservations
cat > features/reservations/reserve_spot.feature <<'EOF'
@REQ-101
Feature: Reserve a parking spot
Scenario: Reserving an available spot for a future window
Given spot "A-12" is free between "09:00" and "11:00"
When an operator reserves spot "A-12" for that window
Then spot "A-12" is marked "Reserved" for that window
And a "SpotReserved" event is emitted
EOF7.3 Add the traceability row β with csda req, not by hand#
The matrix is a ten-column pipe table. Editing it by hand is the single most
common source of broken links in a spec-driven project, and one misplaced |
breaks the row silently. csda req writes it for you:
csda req add "Reserve a parking spot in advance"
# β Added REQ-101 (SCN-101, status Draft)
csda req link REQ-101 \
--feature features/reservations/reserve_spot.feature \
--uc UC-101 --cmd ReserveSpotCommand \
--agg ParkingFacility --evt SpotReservedreq add picks the next free ID, so it cannot collide with the pack's range,
and req link fills the columns you name and leaves the rest alone.
AI_RULES.mdtells every agent never to edittraceability.mddirectly. The same applies to you:csda req,csda doneandcsda change archiveare the three things that write it.
7.4 Validate, plan, implement, close#
csda status # where the project stands, and what is next
csda validate . --strict-tdd # confirms REQ-101 is wired in correctly
csda plan # REQ-101 now appears as pending
# β¦write ReservationServiceTest, then ReservationService.javaβ¦
csda req link REQ-101 --test ReservationServiceTest --code ReservationService.java
csda done REQ-101 --strictThat is the whole consumer loop: spec β scenario β matrix row β validate β plan β implement β done. To put a requirement into the pack so every project gets it, see Step 10.
This is the day-one path. Once a requirement has shipped, changing it goes through the change lifecycle instead β Step 11Β½ β so the modification is reviewable as intent rather than as a diff of the matrix.
8. Step 7 β Upgrade the pack version (specops diff + specops sync)#
π Run from: inside your project β
smart-parking/.β οΈ This is the step people get wrong.
diffandsyncrun from your implementation project, not from the pack repo. The pack repo is just the source.diff/syncreconcile your project against a newer pack version, using the.specops.lockthat lives inside your project. You nevercdintoparking-management-specopsfor this.
Concept#
Time passes. The pack maintainers publish a new git tag β say v0.2.0 β
with new scenarios and fixes. You decide when to adopt it. Two commands:
specops diffβ preview. Renders the pack at the new version into a throwaway temp folder and shows you what would change. Writes nothing.specops syncβ apply. Re-renders the pack and three-way merges the result into your project, preserving your local edits.
8.1 Preview the change β specops diff#
# still inside smart-parking/
csda specops diff --pack-version v0.2.0ββ backend @ v0.2.0 (current: v0.1.0) ββ
+ features/pricing/dynamic_pricing.feature
~ docs/specs/use-cases.md
~ docs/specs/traceability.md
1 added Β· 2 modified Β· 9 unchanged+ is a new file, ~ is a modified one. Nothing is written.
--format json (alias --plan) emits the same data for tooling.
8.2 Apply it β specops sync#
csda specops sync --pack-version v0.2.0sync re-renders the pack and, for every file, compares three versions:
- base β what the pack rendered last time (kept in
.specops/baseline/), - local β what is in your project now (you may have hand-edited it),
- incoming β what the pack renders at the new version.
From that it picks a per-file outcome:
| Outcome | Meaning |
|---|---|
added | new file from the pack β written |
unchanged | identical already β nothing to do |
updated | you never touched it β take the pack's new version |
kept | the pack did not change it but you did β your edit is preserved |
merged | both changed, different lines β merged cleanly |
CONFLICT | both changed the same lines β git-style <<<<<<< markers written |
sync exits non-zero if any file is left in CONFLICT, so CI notices.
Resolve the markers by hand, then re-run.
Flags: --dry-run (preview, write nothing), --force (pack always
wins β discard local edits), --abort-on-conflict (leave conflicting files
untouched instead of writing markers), --pack <id> (sync just one pack).
8.3 After a sync#
csda validate . --strict-tdd
csda plan # new REQs from the pack now show as pending
git add .specops.lock .specops/ docs/ features/
git commit -m "chore: sync parking pack to v0.2.0"Running
csda specops syncwithout--pack-versionjust re-renders everything at the versions already pinned in.specops.lockβ handy after a fresh clone, or to regenerate a file someone deleted.
9. Step 8 β Remove a pack (specops remove)#
π Run from: inside your project β
smart-parking/.
csda specops remove backendremove drops the pack's entry from .specops.lock. It deliberately does
not delete the generated files β you may have hand-edited tests that
point at them. Review with git status and delete what you no longer want
by hand. --dry-run shows what would be removed.
10. Step 9 β Author your own pack#
π¦ Run from: the pack repo β a folder for your pack (e.g.
~/sandbox/domain-packs/β¦), not your implementation project. This is the one place in the tutorial where you leavesmart-parking/.
Concept#
So far you have consumed a pack. Eventually you will want to package
your own domain knowledge so other projects (or your future self) can
specops add it. Four commands cover the authoring lifecycle.
10.1 Scaffold β pack init#
cd ~/sandbox
csda pack init --out ./domain-packs --name "Reservations Backend" --type backend
# flavours: backend Β· frontend Β· contractsWrites ./domain-packs/reservations/backend/pack.yaml plus a templates/
folder.
10.2 Lint β pack lint#
pack lint validates a pack beyond its JSON Schema: unique IDs,
cross-reference integrity, and scenario quality.
csda pack lint --pack-root ./domain-packs --pack reservations/backendThe scenario-quality rules flag vague or thin Gherkin β a Scenario Outline with no Examples, a scenario missing a When/Then, fewer than
three steps, a generic title, vague step language (works, correctly,
as expected, etc, TODO, ...), or a pack.yaml scenario name that
has drifted from its template title.
# In CI β and before a pack feeds the harness β promote those to errors:
csda pack lint --pack-root ./domain-packs --pack reservations/backend --strictThis matters because the pack's scenarios become the reward signal for the harness (Step 11): weak scenarios let the harness wave through weak code.
10.3 See the reference graph β pack lint --graph#
The hardest part of authoring a pack is keeping the
REQ β UC β CMD/QUERY/AGG β EVT cross-references consistent by hand.
--graph draws that spine so you can see it:
# Mermaid (default) β renders natively in GitHub and VS Code
csda pack lint --pack-root ./domain-packs --pack reservations/backend --graph
# Graphviz DOT
csda pack lint --pack-root ./domain-packs --pack reservations/backend --graph --graph-format dotA reference to an ID/name that does not exist becomes a red missing
node in the diagram and is listed on stderr β and the command exits
non-zero, so --graph doubles as a CI link-check.
10.4 Invert the flow β pack infer#
Writing the model first (requirements β use cases β commands β events) and
the scenarios last has a waterfall smell. pack infer flips it: write the
.feature first, get a proposed pack.yaml skeleton back.
csda pack infer --from ./drafts/reserve_spot.featureIt heuristically maps a @REQ-NNN tag β a requirement reference; the
Feature: name β the use case name; each When step β a command; a quoted
PascalCase token in a Then step β an event; each Scenario: β a
scenarios[] entry. Anything it cannot infer is left as an explicit
TODO: β a skeleton to review, never a silent guess. Output goes to stdout
(--format json for tooling); it never mutates pack.yaml.
# Review the proposal, then merge the parts you want:
csda pack infer --from ./drafts/reserve_spot.feature >> domain-packs/reservations/backend/pack.yaml11. Step 10 β Add a NEW requirement (as a pack author)#
π¦ Run from: the pack repo (e.g. a clone of
parking-management-specops, or your own pack folder). This is the other half of "adding a requirement" β Step 6 added one to a single project; this adds one to the pack, so every project thatspecops syncs will receive it.
11.1 Draft the scenario, then infer the model#
# inside the pack repo
cat > drafts/waitlist.feature <<'EOF'
@REQ-006
Feature: Capacity waitlist
Scenario: Joining the waitlist when the facility is full
Given the facility is at full capacity
When a driver requests entry
Then the driver is added to the waitlist
And a "DriverWaitlisted" event is emitted
EOF
csda pack infer --from drafts/waitlist.feature11.2 Merge the inferred skeleton into pack.yaml#
Add the new requirement, use_case, command, event and scenario
entries to pack.yaml, replacing every TODO: with real values and fixing
the IDs so they fit the pack's numbering. Add the feature template under
templates/features/β¦ and point the scenario's template: / target:
fields at it.
11.3 Lint β including the graph#
csda pack lint --pack-root . --pack backend --strict
csda pack lint --pack-root . --pack backend --graph--strict catches a weak new scenario; --graph shows the new
REQ-006 β UC-006 β β¦ β DriverWaitlisted spine and shouts if you mistyped
a reference.
11.4 Version, tag, publish#
# bump metadata.version in pack.yaml (e.g. 0.1.0 β 0.2.0), then:
git commit -am "feat: add capacity waitlist (REQ-006)"
git tag v0.2.0
git push --tags11.5 Consumers adopt it β back in their projects#
Now anyone with a project that uses this pack picks up REQ-006 through
the normal Step 7 flow β from inside their own implementation project:
# π inside smart-parking/ (NOT the pack repo)
csda specops diff --pack-version v0.2.0 # preview: + waitlist feature, ~ matrix
csda specops sync --pack-version v0.2.0 # three-way merge into the project
csda plan # REQ-006 now shows as pendingThat is the full pack-author loop: draft scenario β pack infer β merge
β pack lint --strict --graph β version + tag β consumers diff +
sync.
11ΒΌ. Step 10Β½ β Change a requirement that already shipped#
π Run from: inside your project β
smart-parking/.
Concept#
Step 6 added a requirement on a blank page. This is the other case, and it is the one you hit for the rest of the project's life: REQ-101 shipped, and now it has to change.
Editing spec.md and the matrix directly would work, and it would leave no
record of why. A change makes the modification reviewable as intent β a
delta stating only what moves β and archiving it writes the matrix rows for you.
Open it#
csda change new reservations-need-a-deposit
# β Change reservations-need-a-deposit created (lite Β· REQ ids REQ-102β¦REQ-104 reserved)
csda change status
# β proposal proposal.md
# βΆ specs specs/**/spec.md
# Next β Write specs/**/spec.mdThe reserved ID range means two changes in flight never hand out the same
REQ-NNN.
Write the delta#
Only what moves. csda change instructions specs prints the template, the
rules the validator enforces and your project's declared stack:
# Delta β reservations
## MODIFIED Requirements
### Requirement: REQ-101 β Reserve a parking spot in advance
The system SHALL require a deposit before confirming a reservation, and SHALL
release the spot if the deposit is not paid within 15 minutes.
#### Scenario: SCN-101 β A reservation without a deposit expires
- GIVEN spot "A-12" is reserved but unpaid
- WHEN 15 minutes pass
- THEN the reservation is cancelled and the spot is free again
<!-- csda:trace uc=UC-101 cmd=ReserveSpotCommand agg=ParkingFacility evt=ReservationExpired
feature=features/reservations/reserve_spot.feature -->MODIFIED replaces the whole requirement block; it does not merge scenario by
scenario. Steps are plain - GIVEN bullets, and the body needs SHALL,
MUST, SHOULD or MAY β the validator rejects both alternatives.
Review, archive, implement#
csda change validate # runs inside `csda validate` too
csda change archive reservations-need-a-deposit --dry-run
csda change archive reservations-need-a-deposit --yesArchiving is the part that earns the ceremony. It applies the delta to
docs/specs/capabilities/, writes the traceability rows, copies the
proposed .feature files into features/, and files the change under
docs/specs/changes/archive/<date>-<id>/.
The moment it lands, the requirement is real work again:
csda plan # the modified REQ is pending once more
csda validate . --strict-tdd # fails with [TDD-1] once you move it to In DevIt composes with packs. When the pack itself changes,
csda specops diff --as-changederives exactly this kind of proposal from the version bump β so you review an upstream upgrade as intent rather than as a file diff. That is Step 7 with one extra flag.
β Reviewing changes for the full reference.
11Β½. Bootstrap before the harness (the only freeform-AI step)#
π Run from: inside your project β
smart-parking/.
Concept#
The harness implements one REQ at a time inside an isolated worktree. That is great for iterating, but it does not bootstrap your project: it assumes the build system is already there, the BDD framework is wired, and at least one bounded context runs end-to-end. Phase 1 β that first scaffolding β is the only step where you hand a freeform AI prompt to opencode/Claude/Cursor.
The complete prompt, ready to paste, lives at
docs/bootstrap-prompt.md. It tells the agent:
- it is your Lead Architect;
- the current directory is the only scope;
spec.md,AI_RULES.md,features/**/*.featureare read-only;- to make
csda validate . --strict-tdd+ the project test command pass with the first bounded context end-to-end β then stop.
# 1. Open opencode/Claude/Cursor inside smart-parking/
# 2. Paste docs/bootstrap-prompt.md verbatim, then "go".
# 3. When the agent is done, prove it:
csda validate . --strict-tdd
mvn -B test # or your stack's test command
csda plan # remaining REQs show as pending
git commit -am "phase 1: bootstrap"From this point on the harness takes over β you never paste a prompt
again. The universal directives from bootstrap-prompt.md (Role, Active
Project Boundary, Execution Policy) move into harness.config.yaml so
they ride along on every REQ.
12. Step 11 β Automate delivery with the harness (harness run)#
π Run from: inside your project β
smart-parking/.
Concept#
Everything in Steps 4β6 β read the scenario, write the test, write the
code, run done β is a loop a machine can drive. harness run is that
driver. For each pending requirement, in an isolated git worktree on a
fresh harness/REQ-NNN branch, it:
- builds a self-contained prompt (the Gherkin scenario +
AI_RULES.md+ the exact artifact paths + any previous failure), - shells out to your AI agent,
- gates the result with
validate --strict-tdd+ your test command, - on green β runs
doneand commits; on red β retries, feeding the failure back into the next prompt, - prints a pass/fail/attempts report.
It is vendor-neutral: the agent is any shell command that contains
the {prompt_file} placeholder. The harness never merges a branch β you
review and merge harness/* yourself.
12.1 Configure it β using opencode as the agent, with a project-wide prompt prefix#
Start by generating the two files rather than copying them out of this page:
csda harness initIt writes harness.config.yaml and .harness/prompt-prefix.md, detects your
gate from the build files it finds (mvn -B test here, from pom.xml), and
deliberately leaves agent: unset β which agent runs this is your choice and
your credentials. Read the prefix it generates and make it sound like your
team; the rest of this section is what a filled-in version looks like.
If you drive your editor with opencode, point the
harness at it. The harness writes each prompt to a temp file and substitutes
its path for {prompt_file}; opencode run takes a prompt string, so read
the file in:
# harness.config.yaml β at the root of smart-parking/
harness_version: 1
agent: 'opencode run "$(cat {prompt_file})"'
test_cmd: "mvn -q test"
max_attempts: 3
# Project-wide directives prepended to every per-REQ prompt: Role,
# Active Project Boundary, Execution Policy. Use prompt_prefix for a
# one-liner; prompt_prefix_file for the realistic multi-line case.
prompt_prefix_file: ./.harness/prompt-prefix.md<!-- .harness/prompt-prefix.md -->
# Role
You are the Lead Technical Architect and Senior Backend Engineer.
# Active Project Boundary
- Current directory is the only scope. Do NOT scan siblings.
# Execution Policy
- Start coding. No planning-only output.
- Hexagonal architecture is non-negotiable (see AI_RULES.md).
- Never modify AI_RULES.md, spec.md, or features/\*_/_.feature.With a config file you do not have to retype anything. (Any other agent
works the same way β e.g. claude -p < {prompt_file} or
aider --yes --message-file {prompt_file}.) The prompt_prefix /
prompt_prefix_file rides along on every per-REQ prompt, so your
universal directives are not duplicated and not forgotten.
12.2 Dry-run first#
csda harness run --dry-run--dry-run builds and prints the prompt for every pending requirement
without invoking the agent or touching git. Read what opencode would
receive before you spend tokens.
12.3 Run it#
# the working tree must be clean β the harness refuses a dirty tree
csda harness run --agent 'opencode run "$(cat {prompt_file})"' --test-cmd "mvn -q test"(If you put the agent and test command in harness.config.yaml, plain
csda harness run is enough.)
ββ harness report ββ
β
REQ-002 pass (1 attempt) β harness/REQ-002
β
REQ-003 pass (2 attempts) β harness/REQ-003
β REQ-004 fail (3 attempts) β harness/REQ-004
Gate failed at: test command
2 passed Β· 1 failed Β· 0 skipped
Review and merge the harness/* branches you trust.Flags: --req REQ-NNN (limit to specific requirements, repeatable β
great for trying one first), --max-attempts <n>, --base-branch <ref>,
--timeout <seconds>, --keep-worktrees, --force (recreate existing
harness/* branches), --format json.
The command exits non-zero if any requirement did not pass, so CI can gate on it. Each result is a branch you can check out, inspect, and merge β or throw away.
12.4 Inspect what the agent will receive (harness prompt)#
csda harness prompt REQ-001Prints the exact prompt the harness would hand the agent for one REQ β prefix included β without invoking the agent, creating worktrees, or touching git. Use it to:
- iterate on
AI_RULES.mdandprompt_prefixand see the effect immediately; - copy-paste the prompt into a web AI when no CLI agent is available;
- review what the team's bootstrap directives currently say.
Every prompt the harness actually sends during harness run is also
mirrored to .specops/harness-prompts/REQ-NNN-<timestamp>-attempt-N.md
for after-the-fact review. Commit or gitignore that folder per your
team's preference.
12Β½. Multi-stack: one pack, many implementations#
The pack/implementation split lets you ship the same domain spec in multiple stacks without duplicating requirements or scenarios. Each implementation is its own repo:
parking-management-specops@v0.1.0 βββ one spec, stack-agnostic
ββββΊ smart-parking-spring/ (STACK=Spring Boot, JUnit+Cucumber)
ββββΊ smart-parking-quarkus/ (STACK=Quarkus, JUnit+Cucumber)
ββββΊ smart-parking-micronaut/ (STACK=Micronaut, JUnit+Cucumber)Each project runs its own csda init with a different STACK in the
config, its own csda specops add against the same pack repo +
version + domain --vars, its own bootstrap-prompt run, and its own
harness loop. spec.md, features/** and traceability.md are
identical across stacks (rendered from the same pack); AI_RULES.md
differs because its {{STACK}} substitution does.
Useful for migration POCs, framework comparisons, educational material
("same problem, three stacks"). The architecture explicitly supports it β
see docs/specs/architecture.md Β§5.
13. Step 12 β Companion tooling#
VS Code extension#
The vscode-spec-driven extension turns the editor into a pack-authoring
surface: live JSON Schema squigglies on pack.yaml, dangling-reference
diagnostics, reference-field autocomplete (offers the IDs/names that
actually exist), go-to-definition on a reference, a CodeLens showing
how many use cases and scenarios point at each requirement,
validate-on-save, and the Spec-Driven: Show Pack Graph command β a
side-panel Mermaid render of the pack graph that refreshes as you edit.
MCP server#
The mcp-spec-driven server exposes plan, mark_requirement_done,
read_spec, lint_pack and friends as MCP tools, so an MCP-aware client
(Claude Desktop, Cursor, opencode, Aider) can drive the same loop natively.
Slash commands, for eight tools at once#
csda agents init # or --tool claude,cursorThat writes /csda:explore, /csda:propose, /csda:verify, /csda:apply,
/csda:archive and /csda:onboard, plus the instruction file each tool reads
β .cursor/rules/, .github/copilot-instructions.md, CONVENTIONS.md and so
on. They are thin on purpose: rather than restating the delta grammar, which
would be stale the moment it moved, they call csda change instructions <artifact> --json for it.
After a CLI upgrade, csda update three-way merges those files so your edits
survive.
Keeping an existing repository honest#
This tutorial started from init, on a blank page. On a repository that
already has code, start with csda onboard β it reads the layout and proposes
the capabilities the code already implies β then csda adopt, which writes the
spec skeleton without touching a line of source. csda doctor reports what has
drifted, with a fix per finding.
14. Command cheat-sheet#
The π / π¦ column is the thing to remember β where you run it.
| Command | Run from | What it does |
|---|---|---|
csda init --config <f.yaml> --out <dir> | π parent dir | Scaffold a new spec-driven project |
csda validate <dir> [--strict-tdd] | π project | Check structure, traceability, Gherkin; --strict-tdd adds the TDD gate |
csda plan [--format json] | π project | List requirements still needing work |
csda done REQ-NNN [--check|--strict] | π project | Mark a requirement done in the matrix |
csda status | π project | Daily dashboard: totals, orphans, pack versions, next command |
csda req add|link|done|list | π project | Manage matrix rows without hand-editing the table |
csda fix [--dry-run] | π project | Apply the repairs validate suggests |
csda change new <id> [--lite|--full] | π project | Open a change against specs that already shipped |
csda change status | validate | show | list | π project | What to write next Β· check the deltas Β· inspect Β· enumerate |
csda change archive <id> [--dry-run|--yes] | π project | Merge the delta into the specs + matrix, then file the change away |
csda specops add --pack-repo β¦ --pack-version β¦ --pack β¦ --var β¦ | π project | Pull a pack in; writes .specops.lock + .specops/ baseline |
csda specops diff [--pack-version β¦] | π project | Preview what a sync would change β writes nothing |
csda specops sync [--pack-version β¦] | π project | Re-render + three-way merge the pack into the project |
csda specops remove <pack-id> | π project | Drop a pack from .specops.lock |
csda specops diff --as-change | π project | Turn an upstream pack bump into a reviewable change proposal |
csda specops contribute --change <id> | π project | Send a local change back upstream to the pack (never pushes) |
csda validate --against-lock | π project | Fail CI when the project has drifted from the locked pack version |
csda expand --pack-repo β¦ --pack-version β¦ --pack β¦ | π project | Low-level pack render (what sync calls) |
csda harness run --agent "β¦ {prompt_file}" [--test-cmd β¦] | π project | Run the planβagentβverifyβdone loop per requirement |
csda pack init --out β¦ --name β¦ --type backend|frontend|contracts | π¦ pack repo | Scaffold a pack skeleton |
csda pack lint --pack-root β¦ --pack β¦ [--strict] | π¦ pack repo | Lint a pack: schema, cross-refs, scenario quality |
csda pack lint β¦ --graph [--graph-format mermaid|dot] | π¦ pack repo | Render the pack reference graph; CI link-check |
csda pack infer --from <feature> [--format json] | π¦ pack repo | Propose a pack.yaml skeleton from a .feature |
Three ways to add a requirement, side by side:
| Day one, by hand (Step 6) | As a change (recipe 12) | As a pack author (Step 10) | |
|---|---|---|---|
| Where | π your implementation project | π your implementation project | π¦ the pack repo |
| 1 | edit spec.md | change new <id> | draft a .feature, run pack infer |
| 2 | write features/**.feature | write the delta under specs/<cap>/spec.md | merge the inferred skeleton into pack.yaml |
| 3 | add a row to traceability.md | change validate | add the feature template under templates/ |
| 4 | validate --strict-tdd | change archive β writes the row for you | pack lint --strict --graph |
| 5 | plan β implement β done | plan β implement β done | bump version, tag, push |
| 6 | survives every specops sync | reviewable as intent, archived with a date | consumers specops diff + specops sync |
Use the middle column once the project is live: it is the only one of the three that leaves an audit trail of why a requirement changed, and the only one where you do not touch the ten-column matrix by hand.
You have now exercised every command the tool ships. For deeper reference
see docs/how-to.md, the
domain-pack format spec, the
SpecOps workflow and the
harness spec.