Build with Claude. Your product model keeps up — as markdown in the repo, bound to real symbols in your code, and measured against them.
ds sync reconciles both directions — a spec its code has moved past, and code nothing
in the model describes.
In a Claude Code session, the same thing is /ds:sync.
> add a discount to checkout
Claude edits src/billing/legacy.ts — a file deleted three weeks ago. CLAUDE.md still
describes it, so Claude believed it. You find out in review, or you don't.
CLAUDE.md is what carries knowledge across an empty context window, and nothing checks that it
is still true. dspec makes that checkable — in both directions:
/ds:sync
- [spec older than code] Checkout / Apply discount
the code changed after this spec was written
Code the model does not describe — 2 symbols in 1 area:
billing 2/3
stackCoupons, expireCoupon
The first half is the one every drift tool has. The second half is the one that matters after you have built something: not "which spec has gone stale", but "what did we just ship that nothing describes". Without it, a model can only ever rot politely.
That is also why you do not write the spec first. Claude builds; /ds:sync asks what changed and
what is new; you decide which of it is real product behaviour worth writing down. .ds/ ends up
describing what the product is, never what somebody once proposed.
One markdown file per behaviour, in .ds/. /ds:sync and /ds:bootstrap are what write these —
you read and correct them, which is why the format is small enough to review at a glance:
---
name: Cancel order
kind: write # write = changes state · read = query only
actors: [Customer]
codeRef:
file: src/order/cancel.ts # ← the binding that makes drift measurable
symbol: cancelOrder
verifiedBy: [test/cancel.spec.ts]
---
Rules
- Only a pending @Order may be cancelled
- Cancelling is final — a cancelled @Order never returns to pending
Errors
- ALREADY_SHIPPED when the order has left the warehouseFive labels and nothing else — Rules · Input · Errors · Effects · Visibility. @Order links to
an entity; those links are the only edges, and the map between contexts is derived from them.
The filter for what belongs here:
If one read of the file
codeRefpoints at would tell you, it is not worth a line. Write what that read would not tell you.
codeRef is what turns this from documentation into a measurement. dspec fingerprints the symbol
it names, so "has this gone stale?" is answered by reading your checkout — not by anyone
remembering.
Every session opens knowing where the model and the code stand. Before you type anything:
This repository is modelled in DSpec at `.ds/`, and the model is the source of truth
for domain names, business rules and flows. `CLAUDE.md` is generated from it.
Outstanding right now:
- 2 spec files changed but not committed
- thin spec: Ordering / Cancel order
Do not invent behaviour the model does not describe — read the spec file for it, or ask.
Editing a file surfaces the spec that governs it. Claude is told, mid-session, which usecase binds to the file it just touched — and that leaving the model describing behaviour the code no longer has is a finding, not a detail.
Asking about a piece of work produces a scoped brief. ds pack "<what you are doing>" finds
the elements involved and prints one ring of context around them, plus:
## Code Map
Files bound to the entities in this task. Files not listed here
are unaffected — do not modify them.
- src/order/cancel.ts
- src/order/order.entity.ts
One ring of context — what your usecase mentions and what mentions it — not the whole model, so the brief stays small as the model grows. It also carries a ⚠ block naming every spec in that scope that is thin, unproven, or older than its code: the places Claude must ask rather than infer.
Two steps, in this order. Type both inside Claude Code:
/plugin marketplace add tuna781/dspec # 1. register the marketplace
/plugin install ds@ds # 2. install the plugin from it
Important
Step 2 on its own fails with marketplace ds not found. ds@ds reads as
<plugin>@<marketplace> — the second ds names the marketplace, which does not exist until
step 1 registers it. Both halves really are called ds: the repository is
tuna781/dspec, the marketplace it publishes is named ds.
Requires Claude Code, Node ≥ 20 and git. dspec is written in JavaScript, and Claude Code runs
it by starting a process — so every slash command is really
node "${CLAUDE_PLUGIN_ROOT}/bin/ds.js" …. You never type that; the command carries it. Node has
to be there the same way git does.
dspec itself installs nothing. No npm package, no global binary, nothing of ours on your
PATH. The CLI, the commands, the skill and the hooks all ship inside the plugin, so the hook can
never be a different version from the CLI it calls — and taking a new release is
/plugin update ds and nothing else.
Update: /plugin marketplace update, then /plugin update ds. Check it with /ds:doctor.
Build, then sync. A feature starts as a description, not as a spec file — and .ds/ is
written once the code exists, so the model only ever describes what the product is.
| 1. | /ds:bootstrap — set the repo up and draft a model from the code already here |
| 2. | /ds:spec {what you want} — a detailed statement of it, checked against the model, for you to approve |
| 3. | /ds:plan {what you want} — the implementation plan, then the implementation |
| 4. | /ds:sync — reconcile the model with what you just built |
Step 2 is the one that earns its place. Describing a feature in chat gives you a description that
may contradict a rule you already have, and you find out after the code is written. /ds:spec
reads the model first, writes inside the rules that already exist, and says which ones your
request conflicts with — while it is still a sentence, not a branch.
Important
Only /ds:sync and /ds:bootstrap write to .ds/. /ds:spec ships without file-writing
tools at all, so it cannot leave the model describing something that does not exist yet.
You do not have to type any of it. The Stop hook notices when the code has moved past the specs
bound to it and offers /ds:sync — so the loop closes even for a session that was ordinary chat
from beginning to end.
/ds:bootstrap writes one thing outside .ds/, and you commit it:
That is how a teammate gets all of this: they clone, trust the folder, and Claude Code enables the plugin with no prompting — no install steps for them at all.
/ds:bootstrap |
Set the repo up and draft a first model from the code already here |
/ds:spec |
Describe in detail what you want to build, checked against the model |
/ds:plan |
Plan the implementation, get it approved, build it |
/ds:sync |
Reconcile the model with the code, and fix what is safe to fix |
/ds:check |
Show where the model and the code disagree, changing nothing |
/ds:doctor |
Is this installation healthy? |
ds sync reconciles both directions: specs whose code moved, changed or vanished — and
code that nothing in the model describes. The second half is what makes a code-first loop
possible; without it, nothing could ever notice what you had just built.
--write only re-measures. It re-fingerprints every bound symbol and re-renders the artifacts;
it never rewrites a spec body to agree with the code, and never deletes an element. A spec the
code has overtaken is the place where the code is the unreviewed party.
Three hooks run without being asked — on session start, after an edit, and on stop. They only add context and can never block a tool call.
Everything is local: no server, no token, no network call, no telemetry, and zero runtime dependencies.
| Reference | File syntax, codeRef, drift kinds, lint rules, troubleshooting |
| Design notes | Why the model has this shape, and what dspec refuses to guess |
| Complex domains | When to split a context, shared entities vs. one drawn twice, worked at scale |
| Contributing | Snapshots, the committed dist-plugin/, releasing |
| Security | What it reads, what it writes, what it never does |
MIT — see LICENSE.