create-spec-driven-app v0.7.0

πŸš— 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.

csda is the short name for create-spec-driven-app. If you have not installed it globally, replace every csda in this guide with npx 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. init created it; expand / specops add copied 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#

  1. Prerequisites
  2. Step 1 β€” Scaffold the project (init)
  3. Step 2 β€” Apply the domain pack (specops add)
  4. Step 3 β€” Validate the project (validate)
  5. Step 4 β€” See what is left to build (plan)
  6. Step 5 β€” Implement a requirement and close the loop (done)
  7. Step 6 β€” Add a NEW requirement (as a project consumer)
  8. Step 7 β€” Upgrade the pack version (specops diff + specops sync)
  9. Step 8 β€” Remove a pack (specops remove)
  10. Step 9 β€” Author your own pack (pack init Β· lint Β· --graph Β· infer)
  11. Step 10 β€” Add a NEW requirement (as a pack author) 11Β½. Step 10Β½ β€” Change a requirement that already shipped (change)
  12. Step 11 β€” Automate delivery with the harness (harness run)
  13. Step 12 β€” Companion tooling (VS Code Β· MCP Β· agents)
  14. 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 surface

2. Step 1 β€” Scaffold the project (init)#

πŸ“ Run from: the parent directory (e.g. ~/sandbox). init creates smart-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 project

Flags: --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:

  1. clones the pack repo (at the version you pin),
  2. 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/,
  3. writes a .specops.lock file 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 .feature files under features/ (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 --var values.
  • .specops/baseline/… β€” a verbatim copy of exactly what the pack rendered. This is the "known-good ancestor" that makes a later specops sync able to merge safely instead of clobbering your edits.

βœ… Commit .specops.lock and .specops/ to git. A teammate who clones your project needs both for specops sync to 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).

expand is the low-level version of this. specops add is the friendly wrapper; expand does the same rendering with more explicit flags and no lockfile bookkeeping. Use specops add day 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-tdd

It additionally fails when:

  • a row has a TBD test artifact but a status past Draft β€” [TDD-1],
  • a row has a status but no Scenario ID β€” [TDD-2],
  • a REQ-NNN is mentioned in spec.md but has no row in traceability.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 json

Emits 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:

  1. Read the .feature file plan pointed you at β€” it is the executable spec, the source of truth for behaviour.
  2. Write the test first, so it fails for the right reason (TDD).
  3. Write the production code until the test passes.
  4. 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 --check

done 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 at REQ-101 and 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
EOF

7.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 SpotReserved

req 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.md tells every agent never to edit traceability.md directly. The same applies to you: csda req, csda done and csda change archive are 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 --strict

That 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. diff and sync run from your implementation project, not from the pack repo. The pack repo is just the source. diff/sync reconcile your project against a newer pack version, using the .specops.lock that lives inside your project. You never cd into parking-management-specops for 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.0

sync 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:

OutcomeMeaning
addednew file from the pack β€” written
unchangedidentical already β€” nothing to do
updatedyou never touched it β†’ take the pack's new version
keptthe pack did not change it but you did β†’ your edit is preserved
mergedboth changed, different lines β†’ merged cleanly
CONFLICTboth 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 sync without --pack-version just 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 backend

remove 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 leave smart-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 Β· contracts

Writes ./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/backend

The 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 --strict

This 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 dot

A 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.feature

It 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.yaml

11. 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 that specops 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.feature

11.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 --tags

11.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 pending

That 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.md

The 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 --yes

Archiving 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 Dev

It composes with packs. When the pack itself changes, csda specops diff --as-change derives 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/**/*.feature are 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:

  1. builds a self-contained prompt (the Gherkin scenario + AI_RULES.md + the exact artifact paths + any previous failure),
  2. shells out to your AI agent,
  3. gates the result with validate --strict-tdd + your test command,
  4. on green β†’ runs done and commits; on red β†’ retries, feeding the failure back into the next prompt,
  5. 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 init

It 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-001

Prints 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.md and prompt_prefix and 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,cursor

That 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.

CommandRun fromWhat it does
csda init --config <f.yaml> --out <dir>πŸ“ parent dirScaffold a new spec-driven project
csda validate <dir> [--strict-tdd]πŸ“ projectCheck structure, traceability, Gherkin; --strict-tdd adds the TDD gate
csda plan [--format json]πŸ“ projectList requirements still needing work
csda done REQ-NNN [--check|--strict]πŸ“ projectMark a requirement done in the matrix
csda statusπŸ“ projectDaily dashboard: totals, orphans, pack versions, next command
csda req add|link|done|listπŸ“ projectManage matrix rows without hand-editing the table
csda fix [--dry-run]πŸ“ projectApply the repairs validate suggests
csda change new <id> [--lite|--full]πŸ“ projectOpen a change against specs that already shipped
csda change status | validate | show | listπŸ“ projectWhat to write next Β· check the deltas Β· inspect Β· enumerate
csda change archive <id> [--dry-run|--yes]πŸ“ projectMerge the delta into the specs + matrix, then file the change away
csda specops add --pack-repo … --pack-version … --pack … --var β€¦πŸ“ projectPull a pack in; writes .specops.lock + .specops/ baseline
csda specops diff [--pack-version …]πŸ“ projectPreview what a sync would change β€” writes nothing
csda specops sync [--pack-version …]πŸ“ projectRe-render + three-way merge the pack into the project
csda specops remove <pack-id>πŸ“ projectDrop a pack from .specops.lock
csda specops diff --as-changeπŸ“ projectTurn an upstream pack bump into a reviewable change proposal
csda specops contribute --change <id>πŸ“ projectSend a local change back upstream to the pack (never pushes)
csda validate --against-lockπŸ“ projectFail CI when the project has drifted from the locked pack version
csda expand --pack-repo … --pack-version … --pack β€¦πŸ“ projectLow-level pack render (what sync calls)
csda harness run --agent "… {prompt_file}" [--test-cmd …]πŸ“ projectRun the planβ†’agentβ†’verifyβ†’done loop per requirement
csda pack init --out … --name … --type backend|frontend|contractsπŸ“¦ pack repoScaffold a pack skeleton
csda pack lint --pack-root … --pack … [--strict]πŸ“¦ pack repoLint a pack: schema, cross-refs, scenario quality
csda pack lint … --graph [--graph-format mermaid|dot]πŸ“¦ pack repoRender the pack reference graph; CI link-check
csda pack infer --from <feature> [--format json]πŸ“¦ pack repoPropose 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
1edit spec.mdchange new <id>draft a .feature, run pack infer
2write features/**.featurewrite the delta under specs/<cap>/spec.mdmerge the inferred skeleton into pack.yaml
3add a row to traceability.mdchange validateadd the feature template under templates/
4validate --strict-tddchange archive β€” writes the row for youpack lint --strict --graph
5plan β†’ implement β†’ doneplan β†’ implement β†’ donebump version, tag, push
6survives every specops syncreviewable as intent, archived with a dateconsumers 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.