π End-to-end tutorial β building Smart Parking#
A friendly, step-by-step walk through every command in
specgate. 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.
If you have not installed the CLI globally, replace every
specgatein this guide withnpx @rsaglobaltech/specgate@latest.This tutorial starts from a domain pack. If you are starting from your own plan or a client's brief instead, follow Write the brief and the From scratch journey β
specgate draftturns the brief into requirements module by module. Everything from Step 3 on applies to both.
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* Β· status Β· new Β· check Β· done Β· specops add Β· specops diff Β· specops sync Β· specops remove Β· change Β· 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 β Run the gate (
check) - Step 4 β See what is left to build (
status) - Step 5 β Implement a requirement and close the loop (
done) - Step 6 β Add a NEW requirement (
new) - 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)
- Step 11 β Change a requirement that already shipped (
change) - Step 12 β Automate delivery with the harness (
harness run) - Step 13 β 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/specgate/packs/β¦). After that it is offline.
Optionally install the CLI globally so specgate works everywhere:
npm install -g @rsaglobaltech/specgate@latest
specgate --version # 0.18.0
specgate --help # the six commands you start with: init, draft, status, new, check, done
specgate --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
specgate 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
βββ AGENTS.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 requirement, REQ-000 β The service reports its own health, so the project is already valid; --no-sample-req leaves it out.
docs/specs/traceability.md is generated from spec.md, the scenario
tags and your tests β nobody edits it, and specgate matrix regenerates it.
Now we give the project 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#
specgate 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β¦). - One generated row per scenario in
docs/specs/traceability.md. - 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, --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 β Run the gate (check)#
π Run from: inside your project β
smart-parking/.
Concept#
check is the gate: the one command that says whether the project is
honest. It runs validate --strict β required files, every .feature
claimed by a requirement, scenarios tagged @REQ-NNN @SCN-NNN, valid
statuses, Gherkin that parses, and no requirement past Draft without a test
that names it β and then your own test suite. This is what CI runs, and
it is the same command you run locally.
Tell it once how to run your tests:
specgate config set test_cmd "./mvnw -B verify" # or mvn -B test, npm test, pytestβ¦ β test_cmd = ./mvnw -B verify
harness.config.yaml
`check` and `done` now run it.Do it#
specgate checkRight after specops add it passes: every pack requirement is still
Draft, and a Draft requirement owes nothing yet. Without a test command it
still passes, and says so β "this checked the specification, not the
code" β because a gate that never ran your tests should not claim it did.
The rule that matters most is [TDD-1]: a requirement whose status has
left Draft must have a test that mentions its id. That mention is the
whole link β there is nothing to register. [TDD-2] (a row with no scenario)
and [TDD-3] (a REQ-NNN in spec.md with no row) catch the rest.
Put specgate check in CI with specgate ci init.
5. Step 4 β See what is left to build (status)#
π Run from: inside your project β
smart-parking/.
Concept#
After specops add, your project has scenarios but no code. status
answers "what do I do next?" β requirement by requirement, what each one
still needs, and the single next command to run.
Do it#
specgate status π Project status smart-parking
Requirements 6 total Β· 0 done Β· 6 pending
To do
REQ-000 Health baseline a test and code
REQ-001 Monitor Capacity Threshold a test and code
REQ-002 Register Vehicle Entry a test and code
REQ-003 Calculate Parking Fee a test and code
REQ-004 Detect Overstay a test and code
REQ-005 Send Payment Receipt a test and code
Packs (.specops.lock)
backend @ v0.1.0
Next specgate done REQ-000 β once a test mentions REQ-000 β that mention is the link; done checks firstStart every day with it; you never have to remember which command applies.
Machine-readable mode#
specgate plan --jsonplan is the detailed view behind status: per requirement, whether its
feature, test and code exist, in a bucket (NEEDS_FEATURE,
NEEDS_EVERYTHING, NEEDS_TEST, NEEDS_IMPLEMENTATION,
NEEDS_STATUS_UPDATE, DONE). Its JSON is the task queue the harness
consumes in Step 12, and what an 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#
The loop for every requirement is the same four moves:
- Read the requirement in
spec.mdand its.featurefile β the executable spec, the source of truth for behaviour. - Write the test first, and name the requirement in it β
REQ-001in the test name or a comment. That mention is the link. - Write the production code until the test passes.
- Close it with
done, which runs the gate first.
Do it#
done refuses while nothing proves the requirement:
specgate done REQ-001 β --strict-tdd violations detected:
[TDD-1] Test artifact is TBD but status is 'Implemented' (scenario: SCN-001)
TDD-1: Write a test that mentions the requirement (REQ-NNN) β that is the link; β¦
β REQ-001 not marked Implemented: validation failed. [done_validate_failed]
fix: Fix what validate reports, then run `done` again.Write the test, naming the requirement and its scenario:
// REQ-001 SCN-001: operators are alerted at the capacity threshold
@Test void alertsAtNinetyPercentOccupancy() { β¦ }β¦then the code, until your suite passes, and run it again:
specgate done REQ-001β REQ-001 β Implemented (1 row updated)done runs the gate β the specification and your test_cmd β and marks
the requirement Implemented only if it passes. --status Verified (or
Released) targets a later state; done REQ-001 REQ-002 β¦ closes several
with one run of the suite; --no-check skips the gate, and says so.
7. Step 6 β Add a NEW requirement (new)#
π 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, not the pack's.
A requirement lives in three places: its section in spec.md, a tagged
scenario under features/, and its row in the matrix. specgate new writes
all three at once.
Do it#
specgate new "Operators can reserve a parking spot in advance"β REQ-006 Operators can reserve a parking spot in advance (Draft)
spec.md ## REQ-006 β rewrite the obligation
features/operators-can-reserve-a-parking-spot.feature @SCN-006 β fill in the <placeholders>
docs/specs/traceability.md row linked to the scenario
Next: write a test that mentions REQ-006 β that is the link β then specgate checkIt takes the next free id. Now make it real:
In
spec.md, replace the placeholder sentence with the real obligation:markdown ## REQ-006 β Operators can reserve a parking spot in advance An operator MUST be able to reserve a specific spot for a future time window, so a known vehicle is guaranteed space on arrival.In the
.featurefile, replace the<placeholders>with a real example β keep the@REQ-006 @SCN-006tags, they are what the matrix reads:gherkin Feature: Operators can reserve a parking spot in advance @REQ-006 @SCN-006 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 windowThen the same loop as Step 5: a test that names
REQ-006, the code,specgate done REQ-006,specgate check.
While it is Draft, the gate leaves it alone; once it leaves Draft, the
gate refuses any <placeholder> left in its scenario.
A whole new module, not one requirement? Write it down in
docs/brief.mdand letspecgate draft <module>turn it into requirements β see Write the brief.Watch the ids when you upgrade the pack. Your local requirement took the next free id,
REQ-006. If a later pack version adds its ownREQ-006,specops diffshows the new feature file andcheckfails with a duplicate scenario id after the sync. Read the diff before syncing, and renumber your local requirement first if the pack claims its id.
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/
specgate specops diff --pack-version v0.2.0 \
--var STACK="Quarkus 3.x, Java 21" --var API_STYLE="REST with DTO boundaries" \
--var TESTING="JUnit 5, Cucumber" --var LANG=enββ backend @ v0.2.0 (current: v0.1.0) ββ
+ features/waitlist/capacity_waitlist.feature
~ AI_RULES.md
~ docs/specs/commands.md
~ docs/specs/domain-model.md
~ docs/specs/events.md
~ docs/specs/traceability.md
~ docs/specs/use-cases.md
1 added Β· 6 modified Β· 7 unchanged+ is a new file, ~ is a modified one. Nothing is written. --json emits
the same data for tooling.
Why the --vars? v0.2.0 of this pack requires four variables v0.1.0
did not. Without them diff and sync stop with
Missing required variable 'STACK'. Provide it using --var STACK=.... Pass
them once; sync writes them back to .specops.lock.
8.2 Apply it β specops sync#
specgate specops sync --pack-version v0.2.0 \
--var STACK="Quarkus 3.x, Java 21" --var API_STYLE="REST with DTO boundaries" \
--var TESTING="JUnit 5, Cucumber" --var LANG=ensync 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#
specgate status # new REQs from the pack now show as pending
specgate check
git add .specops.lock .specops/ docs/ features/
git commit -m "chore: sync parking pack to v0.2.0"Read
statusafter a sync. A requirement you had closed can come back as Ready to close when the sync rewrote its files β runspecgate donefor it again; its test is still there, so the gate passes.
Running
specgate 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/.
specgate 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
specgate pack init --out ./domain-packs --name "Reservations Backend" --type backend
# flavours: backend Β· frontend Β· mobile Β· 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.
specgate 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:
specgate pack lint --pack-root ./domain-packs --pack reservations/backend --strictThis matters because the pack's scenarios become the reward signal for the harness (Step 12): 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
specgate pack lint --pack-root ./domain-packs --pack reservations/backend --graph
# Graphviz DOT
specgate 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.
specgate 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 or a
query; 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.
Five things it does that are worth knowing, because each one used to be a correction you made by hand:
The same operation is proposed once. A happy path and its failure exercise one command; they used to arrive as two entries with two ids.
A
Whenthat reads becomes a query, with aQRY-id β "searches", "views", "lists". The pack model already separates queries from commands; inference now does too.Names drop the glue. "10 more vehicles enter the facility" gives
MoreVehiclesEnterFacility, notMoreVehiclesEnterThe.Field names are proposed from the Gherkin β
<placeholders>in a Scenario Outline andkey: valuepairs β each with aTODOfor its type. The names are in the file; the types are not, and a guessed type that looked confident is exactly what ADR-0014 refuses.--fromrepeats, and takes a directory. A capability is rarely one file, and inferring per file proposes a shared command once per file:bash specgate pack infer --from ./drafts/ # every .feature in it specgate pack infer --from a.feature --from b.feature
# Review the proposal, then merge the parts you want:
specgate 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
specgate 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#
specgate pack lint --pack-root . --pack backend --strict
specgate 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)
specgate specops diff --pack-version v0.2.0 --var β¦ # preview: + waitlist feature, ~ matrix
specgate specops sync --pack-version v0.2.0 --var β¦ # three-way merge into the project
specgate status # 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.
12. Step 11 β Change a requirement that already shipped (change)#
π Run from: inside your project β
smart-parking/.
Concept#
Step 6 added a requirement on a blank page. This is the other case, the one you hit for the rest of the project's life: a requirement shipped, and now it has to change. A change makes the modification reviewable as intent β a delta stating only what moves β and archiving it rewrites the specification, the scenario and the matrix row for you.
change works on the requirements under docs/specs/capabilities/ β the
ones changes and drafts create. A requirement
written with specgate new lives only in spec.md: change that one by
editing its section and its scenario, and check and done hold it to the
same gate.
Bring a capability in through a change#
Say the reservation feature arrives as a change instead of a one-line new:
specgate change new add-reservations --capability reservations
# β Change add-reservations created (lite Β· REQ ids REQ-007β¦REQ-009 reserved)The reserved range means two changes in flight never hand out the same
REQ-NNN. Write the delta in
docs/specs/changes/add-reservations/specs/reservations/spec.md β
specgate change instructions specs prints the template and the rules:
# Delta β reservations
## ADDED Requirements
### Requirement: REQ-007 β Reserve a spot in advance
The system SHALL let an operator reserve a free spot for a future window.
#### Scenario: Reserving an available spot
- 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 windowTick its tasks.md, then archive:
specgate change validate add-reservations
specgate change archive add-reservations
# β Archived as 2026-10-11-add-reservations
# specs: 1 added Β· 0 modified Β· 0 removed
# traceability: 1 row(s) added Β· 0 updated Β· 0 removedChange it later#
Months on, reservations need a deposit. Open a second change and write only what moves:
specgate change new reservations-need-a-deposit# Delta β reservations
## MODIFIED Requirements
### Requirement: REQ-007 β Reserve a 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: 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 againMODIFIED 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.
specgate change archive reservations-need-a-deposit --dry-run # the plan, nothing written
specgate change archive reservations-need-a-deposit
# β Archived as 2026-10-11-reservations-need-a-deposit
# specs: 0 added Β· 1 modified Β· 0 removed
# β² features/reservations/REQ-007.feature β¦ rewritten from REQ-007's modified scenarios.Archiving applies the delta to docs/specs/capabilities/, rewrites the
scenario under features/, updates the matrix row, and files the change
under docs/specs/changes/archive/<date>-<id>/. The requirement is real work
again: specgate status lists what it needs, and done refuses it until a
test covers the new behaviour.
It composes with packs. When the pack itself changes,
specgate 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.
β Reviewing changes for the full reference.
13. Step 12 β 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 +
AGENTS.md+ the exact artifact paths + any previous failure), - shells out to your AI agent,
- gates the result with
validate --strict+ 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.
13.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:
specgate 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 AGENTS.md).
- Never modify AGENTS.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.
13.2 Dry-run first#
specgate 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.
13.3 Run it#
# the working tree must be clean β the harness refuses a dirty tree
specgate 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
specgate 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.
13.4 Inspect what the agent will receive (harness prompt)#
specgate 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
AGENTS.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 standing 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.
13Β½. 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 specgate init with a different STACK in the
config, its own specgate specops add against the same pack repo +
version + domain --vars, and its own harness loop. spec.md and
features/** are identical across stacks (rendered from the same pack).
AGENTS.md differs because its {{STACK}} substitution does, and so does
each traceability matrix: it maps a requirement to the file that implements
and the file that proves it, and those files differ per stack. In one
repository, specgate init --multi-stack spring,quarkus,micronaut sets up the
same arrangement under one root.
Useful for migration POCs, framework comparisons, educational material
("same problem, three stacks"). The architecture explicitly supports it β
see docs/specs/architecture.md Β§5.
14. Step 13 β 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#
specgate mcp install --client claude # or cursor β writes the client's MCP config
specgate mcp serve # what that config starts, over stdioThe server exposes the CLI as MCP tools β specgate_status, specgate_check,
specgate_done, specgate_change_new and the rest β so an MCP-aware client
can drive the same loop natively. See MCP server.
Slash commands, for ten agent tools at once#
specgate agents init # or --tool claude,cursorThat writes /specgate:explore, /specgate:draft, /specgate:new, /specgate:apply, /specgate:check,
/specgate:done, /specgate:propose, /specgate:archive and /specgate: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 specgate change instructions <artifact> --json for it.
After a CLI upgrade, specgate 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, specgate init adopts it: it reads the layout, proposes the
capabilities the code already implies and writes the spec skeleton without
touching a line of source. specgate doctor reports what has
drifted, with a fix per finding.
15. Command cheat-sheet#
The π / π¦ column is the thing to remember β where you run it.
| Command | Run from | What it does |
|---|---|---|
specgate init [--config <f.yaml> --out <dir>] | π parent dir | Scaffold a new project, or adopt a repository that already has code |
specgate draft [<module>] | π project | From a plan in docs/brief.md to requirements, one module at a time |
specgate status | π project | What each requirement still needs, and the next command to run |
specgate new "<title>" | π project | One requirement: its section, its tagged scenario, its row |
specgate done <REQ-NNN β¦> [--status <S>] | π project | Run the gate, then mark it Implemented β or refuse |
specgate check | π project | The gate: validate --strict, then your test_cmd |
specgate config set test_cmd "<cmd>" | π project | Tell check and done how to run your tests |
specgate matrix [--check] | π project | Regenerate the traceability matrix from spec, tags and tests |
specgate plan [--json] | π project | The detailed per-requirement view behind status |
specgate fix [--dry-run] | π project | Apply the repairs validate suggests |
specgate change new <id> [--capability <c>] | π project | Open a change against specs that already shipped |
specgate change status | validate | show | list | π project | What to write next Β· check the deltas Β· inspect Β· enumerate |
specgate change archive <id> [--dry-run] | π project | Apply the delta to the specs, scenarios and matrix, then file the change |
specgate specops add --pack-repo β¦ --pack-version β¦ --pack β¦ --var β¦ | π project | Pull a pack in; writes .specops.lock and the .specops/ baseline |
specgate specops diff [--pack-version β¦] [--var β¦] | π project | Preview what a sync would change β writes nothing |
specgate specops sync [--pack-version β¦] [--var β¦] | π project | Re-render and three-way merge the pack into the project |
specgate specops remove <pack-id> | π project | Drop a pack from .specops.lock (files stay) |
specgate specops diff --as-change | π project | Turn an upstream pack bump into a reviewable change |
specgate specops contribute --change <id> | π project | Send a local change back upstream to the pack (never pushes) |
specgate validate --against-lock | π project | Fail CI when the project drifted from the locked pack version |
specgate harness run [--agent "β¦ {prompt_file}"] [--req β¦] | π project | Agent builds each pending requirement on its own branch; the gate decides |
specgate agents init [--tool β¦] | π project | Slash commands and instruction files for your agent tools |
specgate mcp serve | π project | The MCP server over stdio |
specgate pack init --out β¦ --name β¦ --type backend|frontend|contracts|mobile | π¦ pack repo | Scaffold a pack skeleton |
specgate pack lint --pack-root β¦ --pack β¦ [--strict] | π¦ pack repo | Lint a pack: schema, cross-refs, scenario quality |
specgate pack lint β¦ --graph [--graph-format mermaid|dot] | π¦ pack repo | Render the pack reference graph; CI link-check |
specgate 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 (Step 6) | As a change (Step 11) | As a pack author (Step 10) | |
|---|---|---|---|
| Where | π your implementation project | π your implementation project | π¦ the pack repo |
| 1 | specgate new "<title>" | change new <id> --capability <c> | draft a .feature, run pack infer |
| 2 | rewrite its section and scenario | write the delta under specs/<cap>/spec.md | merge the inferred skeleton into pack.yaml |
| 3 | a test that names the REQ | change validate, then change archive | add the feature template under templates/ |
| 4 | done, then check | a test that names the REQ, done, check | pack lint --strict --graph |
| 5 | β | reviewable as intent, archived with a date | bump version, tag; consumers specops diff + sync |
In none of them do you edit the matrix: it is generated. Use the middle column once the project is live β it is the one that leaves an audit trail of why a requirement changed.
You have now exercised the commands a project uses day to day. For deeper reference
see docs/how-to.md, the
domain-pack format spec, the
SpecOps workflow and the
harness spec.