Maintain product specs¶
Add a new product rule to your spec corpus, share provisions across a group
of rules, or replace a rule — without ever breaking a citation someone has
written down. Writing specs needs an agent that can run commands — a
coding agent such as Claude Code, Codex or Cursor with the hadron CLI
(v0.17.0 or later) — or you at a terminal.
A chat-only app connected to Hadron can read specs and draft with you, but can't save through the spec tooling. If you only need to find and read a spec, see Read and cite product specs.
Ask your agent¶
Most maintenance is a request to a coding agent that has the hadron CLI —
you describe the rule and where it belongs, the agent drafts it and previews
the result, and you approve before anything is saved:
In acme.com:specs, add a new rule under feature api:010 (onboarding
reminders): when a user completes setup, we send a congratulations message
within one hour. Draft it, tell me the citation it would get, and show it to
me, and wait for my OK before saving.
What comes back: the citation it would allocate (the next free number in
that feature, for example api:010:04), the edges it would create — the
link to its feature and to the feature's shared contract — and the draft
itself. What to check before you approve:
- The placement. A citation is permanent, so make sure the rule sits in the feature you meant. If you named the feature loosely, a good agent says where it actually put it; confirm that.
- What the agent had to guess. A short request leaves product decisions open, and a good draft lists them instead of deciding them. Answer them or strike them.
- Any follow-up links. An agent may propose linking the new rule to related specs. Those links can only be created after the rule is saved, so they aren't in the preview; approve them separately.
Manage product specs with a coding agent is the full plain-language workflow, including amending and superseding. The rest of this page is what the agent does underneath, for maintainers who want the exact commands and rules.
What the tooling does¶
hadron spec is the opinionated layer over node/edge. It creates a spec
at the loc you name, or allocates the next number in a numbered corpus. It
scaffolds the rubric, wires the edges you ask for, and never renumbers, so a
citation stays a permanent handle. The full command surface is in the
hadron CLI reference.
Prerequisites¶
- The hadron CLI, v0.17.0 or later, installed and signed in — see
Install the hadron CLI. Check with
hadron auth status. Older CLIs enforce the numbering on every command, have nospec new <loc>orsupersede --to, and still acceptdescribe --declare. - Write access to a spec memory. The examples below use
acme.com:specs, numbered<module>:<feature>:<rule>, andacme.com:platform-specs, numbered with a product in front (<product>:<module>:…). - Familiarity with how a citation reads — Read a citation covers the numbering convention and the level names used throughout this page.
Set your corpus once so you can drop -m from every command:
The examples below keep -m explicit, since a maintainer often works across
more than one corpus.
See what a corpus holds¶
spec describe inventories a memory's specs: how many there are, their root
segments, the deepest loc, and how many follow the numbering convention:
It classifies nothing, and there is nothing to set up before your first spec. Any valid loc is a spec address, and one memory can hold numbered specs and specs at named paths side by side.
Earlier CLIs let a memory declare a flat or product-rooted "scheme" with
spec describe --declare. That flag is retired: it is refused (exit 2) and
writes nothing. A declaration still stored in a memory's data is shown by
describe as retired, and nothing reads it.
Decide whether it belongs¶
Write a spec when a change introduces a product-level rule a product manager would need to read — a threshold, a time window, a categorical rule, or a lifecycle / eligibility / delivery contract. Pure implementation details (file paths, query shapes, caching) do not belong here.
Then decide where it goes. Follow the pattern the corpus already uses: in a
numbered corpus, pick the module (a three-letter code, e.g. api for
messaging) — and, if the corpus numbers products, the product (e.g.
srv) — then the feature the rule belongs to. See what already exists and
which numbers are free:
spec register prints the ledger for the numbered specs, derived from the
live nodes, including the next free feature per module and next free rule per
feature. Specs at any other loc are listed by name under outsideNumbering
rather than left out.
Create a spec at the loc you choose¶
When the corpus isn't numbered, or the rule doesn't fit its numbering, name the loc yourself. If a coding agent drives the CLI for you, say what the spec is for, where it goes, and that you want to see it first:
In acme.com:specs, add a new spec at onboarding:mentor:screens:settings —
the mentor settings screen: it lets a mentor change their notification
preferences and pause new mentee requests. Draft it, show it to me, and
wait for my OK before saving.
The agent checks that the loc is free, drafts the abstract and the rubric
sections, and previews the spec with --dry-run — which writes nothing — then
shows you the draft. It saves only after you approve. Read the draft for what
the agent had to guess: a short request leaves product decisions open, and a
good draft lists them rather than deciding them. (A plain-text preview doesn't
show the abstract; ask to see it, or have the agent use --json.)
To do the same by hand, preview first — --dry-run writes nothing:
hadron spec new -m acme.com:specs onboarding:mentor:screens:settings \
--title "Settings screen" --dry-run
would create onboarding:mentor:screens:settings — onboarding:mentor:screens:settings — Settings screen
tags: [spec]
That creates exactly that spec. No parent is required, no contract is
created, and no number is allocated. The one thing taken from the loc is its
sort order: if the last segment is a number, it sets the spec's seq. It
gets no edges unless you ask for one. --inherit <loc> adds an inheritance
edge to a
general-provisions contract, and
spec link adds anything else
afterward. A loc that already holds a node is refused (exit 2), never
overwritten. The body is the same rubric template as below unless you supply
your own with --content / --content-file; --abstract / --abstract-file
set the abstract only.
A positional loc can't be combined with the numbering flags (--product,
--module, --feature, --rule, --new-*, --contract). Those allocate a
number instead, as the next sections show.
Allocate the next number in an existing feature¶
In a numbered corpus, when the module and feature already exist, you're just
allocating the next rule. Preview it first — --dry-run writes nothing:
hadron spec new -m acme.com:specs --module api --feature 010 \
--title "W4 — 7-day check-in" --dry-run
The output shows the citation it would allocate (the next free rule under
api:010 — here api:010:04), the node name and tags, and the
table-of-contents and inheritance edges it would create. When it looks
right, create it — supplying the abstract and body up front so the spec is
complete:
hadron spec new -m acme.com:specs --module api --feature 010 \
--title "W4 — 7-day check-in" \
--abstract "Re-engage users who signed up but never finished setup, 7 days in." \
--content-file w4.md
Without --abstract / --content-file, the spec is created from a rubric
template — a placeholder abstract and the four mandatory section
headings (Definition, Rule & examples, Durable vs tunable, What invalidates
this spec) — which you fill in afterward.
To change a spec that already exists, see Amend a spec: it
covers the amend-vs-supersede decision and spec edit, which opens the body
and abstract together because they are one logical unit.
Scaffold a whole branch¶
In a numbered corpus, a new rule usually needs a new feature, and sometimes a
new module or product above it. --new-path creates the citation you name
plus every missing ancestor in one call:
✓ created srv:api:010:01 — srv:api:010:01 — Place new order
edge: Place new order → srv:api:010
edge: inherits the shared contract (general provisions) → srv:api:010:00
✓ created srv — srv — srv
✓ created srv:gen — srv:gen — srv general provisions
edge: srv general provisions → srv
✓ created srv:api — srv:api — api
edge: api → srv
edge: inherits the shared contract (general provisions) → srv:gen
✓ created srv:api:000 — srv:api:000 — api general provisions
edge: api general provisions → srv:api
✓ created srv:api:010 — srv:api:010 — 010
edge: 010 → srv:api
edge: inherits the shared contract (general provisions) → srv:api:000
✓ created srv:api:010:00 — srv:api:010:00 — 010 general provisions
edge: 010 general provisions → srv:api:010
Your --title goes on the citation you named. The ancestors get
placeholder titles taken from their own citation segment — srv:api is
titled api, srv:api:010 is titled 010. They lint clean (the name leads
with the citation), so nothing will remind you. Rename them afterward:
--dry-run previews the whole chain the same way.
--new-path only builds numbered chains. Given a loc outside the numbering, it
refuses (exit 2) and tells you to drop --new-path, because
spec new <loc> creates that spec
directly and has no ancestors to build.
Every root also gets its contract¶
Creating a root — with --new-path, or with --new-product /
--new-module / --new-feature — also scaffolds that tier's
general-provisions contract, which is why seven nodes appear above for one
rule. Each root's children inherit that contract, and creating it up front is
what lets the inheritance edges wire themselves:
| Creating this root | also creates |
|---|---|
a product (srv) |
srv:gen |
a module (srv:api) |
srv:api:000 |
a feature (srv:api:010) |
srv:api:010:00 |
Pass --no-contract to suppress it. You rarely want to — adding a contract
to a tier later means
back-wiring its existing siblings
by hand.
Building a corpus one tier at a time¶
--new-path is the short form of a top-down build. The explicit form is
still there when you want to title and populate each tier as you go:
M=acme.com:platform-specs
hadron spec new -m $M --new-product --product srv --title "Server"
hadron spec new -m $M --product srv --new-module --module api --title "Ordering API"
hadron spec new -m $M --product srv --module api --new-feature --title "Order placement"
hadron spec new -m $M --product srv --module api --feature 010 --title "Place new order"
Each level must exist before its children, and each of the first three calls
also creates its contract, exactly as above. Product and module codes are
frozen once created — spec new refuses to mint one that already exists
(exit 5).
Everything else — list --prefix srv, lint, find, supersede — works
the same when the numbering carries a product; the citations simply start
with it (srv:api:010:01).
When scaffolding fails¶
Two different failures look similar and are worth telling apart. Both exit non-zero, but only one of them writes anything.
A missing parent tier is rejected up front (exit 4). Nothing is created:
Create the parent tier and re-run — or use --new-path, which mints the
missing ancestors for you.
An edge that can't be wired fails after the node is created (exit 1). A
spec without its table-of-contents and inheritance edges is silently
orphaned, so spec new refuses to report ✓ created and tells you what's
missing instead:
hadron: created srv:api:010:01 but failed to wire 1 required edge(s) to
srv:api:010 — the node is orphaned; fix the target(s) and re-run, or wire
the edge(s) with `hadron edge add`
This one genuinely half-worked: the node exists. Fix the edge target and either re-run or wire the edge by hand — don't assume a clean slate.
Share provisions across siblings¶
When a provision is shared by a group of sibling specs — every rule of a feature, every feature of a module, or every module of a product — put it in a general-provisions contract instead of repeating it. A spec inherits the contract through an inheritance edge, and the edge is the whole mechanism: a spec with the edge is bound by the contract, and a spec without it is not.
In a numbered corpus the contract has a reserved number, and allocating a spec
with spec new wires the edge to it for you. Roots created by spec new
already have theirs; to add one to a tier that doesn't, use --contract at
the deepest level you name:
| Shared across | Contract citation | Create with |
|---|---|---|
| every rule of a feature | api:010:00 |
spec new --module api --feature 010 --contract |
| every feature of a module | api:000 |
spec new --module api --contract |
| every module of a product | srv:gen |
spec new --product srv --contract |
The numeric tiers use their reserved zero (00, 000); the alpha product
tier uses the reserved code gen.
Outside the numbering there is no reserved place. A contract is an ordinary
spec at a loc you choose, and each spec that shares its provisions inherits it
through --inherit <loc> when you create it, or through
spec link afterward.
Add a contract to a tier that already has siblings¶
Introducing a contract into a tier that already has siblings binds only the
specs you connect to it. spec new --contract creates the contract, but the
pre-existing siblings carry no edge to it, and nothing adds one for you or
reports that it is missing. Back-wire each sibling that should be bound with
spec link, using the inheritance label spec new would have written:
for f in api:010 api:020 api:030; do
hadron spec link "$f" api:000 -m acme.com:specs \
--label "inherits the shared contract (general provisions)"
done
--dry-run previews each link. The same back-wiring applies at every level —
feature siblings to a module's :000, or modules to a product's :gen — not
just the module level shown here.
Decide which siblings inherit¶
Whether a sibling inherits a contract is your decision, and the edge records it. It can feel forced when a group is heterogeneous — say a module whose contract states a scoring model, but one of its features is an A/B assignment mechanism rather than a scoring rule. Read the inheritance edge as "governed by these general provisions," not "is another instance of the same kind." If the outlier is genuinely bound by the shared provisions, give it the edge. If it isn't, leave the edge off rather than stretching the contract to fit.
Lint it¶
spec lint checks a spec against the rubric and the stability rules. A
freshly scaffolded rule fails until you replace the placeholder abstract and
state what invalidates it:
hadron spec lint api:010:04 -m acme.com:specs # one spec
hadron spec lint --prefix api:010 -m acme.com:specs # a feature and its rules
hadron spec lint --module api -m acme.com:specs # a whole module
hadron spec lint --product srv -m acme.com:platform-specs # a whole product
hadron spec lint --all -m acme.com:specs # the corpus
Errors exit with code 5: a missing abstract, no "what invalidates" statement,
a name that doesn't lead with the citation, two specs at the same loc, or an
abstract close to the server's length cap. Warnings do not, unless you pass
--strict. They include a missing data.version, an abstract that may no
longer match its body, a body that is still the unedited scaffold, and a spec
that carries the spec role but not the spec tag. That last one matters:
spec list, get --prefix, grep, replace and check-tools find specs
by the tag, so they skip it. Fix the findings and re-run until it is clean.
The rubric only checks numbered rules and flows. The abstract and "what
invalidates" checks run on specs at rule or flow depth in the numbering
convention (at flow depth as warnings). A spec at any other loc, such as
onboarding:mentor:screens:settings, still gets the name, tag, duplicate and
abstract-freshness checks, but a missing abstract or "what invalidates"
statement is not reported. Review those by eye.
Lint asks nothing about a spec's position: no parent, no inheritance edge, no contract, and no index of children is required, at any loc.
Restructure without breaking citations¶
Three commands cover the common reshaping moves, all of which preserve existing numbers:
spec extractsplits a sub-rule out of a fat parent into its own citation under another feature, piping the moved chunk in as the new body and auto-wiring a cross-reference edge back.--strip-sourcealso trims the chunk from the source, but only when it matches verbatim. It allocates in the numbering, so the source must be a numbered spec. For a spec at any other loc, create the new spec withspec new <loc>and trim the source withspec edit.spec linkcross-references one spec from another by bare citation, validating that both are specs in the same corpus. The convention is that the more specific spec cites the more general one.spec replacedoes a citation-aware find/replace across every spec's body and abstract. It is word-boundary-aware by default, so renamingh-read-nodenever touchesh-read-nodes. Preview with--dry-run, cap the blast radius with--max-specs N, and pass--yesnon-interactively; changed specs are re-linted afterward.
Replace a spec without renumbering¶
A citation is a permanent handle, so you never change a number in place. To
replace a spec, supersede it: spec creates the replacement, links the
old spec to the new one with a superseded-by edge, and tags the old spec
superseded (its loc is untouched). Like the other destructive commands, it
requires --yes when run non-interactively. Preview it with --dry-run.
For a numbered rule or flow, spec mints the next free number in the same
feature (--feature moves the replacement to another existing feature):
For any other spec, name the replacement's loc with --to. It must be a free,
valid loc, and nothing else is derived from it:
hadron spec supersede onboarding:mentor:screens -m acme.com:specs \
--to onboarding:mentor:screens-v2 --title "Screens v2" --copy-body --yes
Without --to, a spec outside the numbering is refused (exit 2) with nothing
written, because there is no number to allocate for it. --to works on a
numbered spec too, when you'd rather choose the replacement's loc yourself.
--copy-body seeds the replacement from the old spec's content and abstract.
Afterward, mark the old number retired in the register ledger — the command
prints a reminder, and never edits the register itself.
Keep the corpus honest¶
Two checks are worth running in CI; both exit 5 on findings.
hadron spec lint --all -m acme.com:specs # rubric + stability rules
hadron spec check-tools -m acme.com:specs # hadron_* tool references
hadron spec register --check -m acme.com:specs # ledger drift vs live nodes
spec check-tools flags hadron_* tool names in spec prose that aren't real
registered tools — the drift that lets stale shorthand rot unnoticed.
spec register --check diffs the hand-written register ledger against the
live nodes; the register is advisory and spec never writes it.
Related¶
- Read and cite product specs — the read side: search, resolve a citation, and follow inherited provisions.
- hadron CLI reference → Product specs — the full command surface, numbering policy, and semantics.
- Use Obsidian to view/edit memory — open the synced or exported repo as an Obsidian vault, and make citation-numbered nodes readable.
- Product specs and citations — the model behind the corpus: general-provisions inheritance, supersede-never-renumber, and the durable-vs-tunable split that decides every change.