Skip to content

Maintain product specs

CLI onlyAdvanced~20 min

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 no spec new <loc> or supersede --to, and still accept describe --declare.
  • Write access to a spec memory. The examples below use acme.com:specs, numbered <module>:<feature>:<rule>, and acme.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:

hadron spec use acme.com:specs

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:

hadron spec describe -m acme.com:platform-specs

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:

hadron spec list -m acme.com:specs --prefix api
hadron spec register -m acme.com:specs

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:

hadron spec new -m acme.com:platform-specs srv:api:010:01 \
  --new-path --title "Place new order"
✓ 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:

hadron node update hrn:node:acme.com:platform-specs:srv:api --name "srv:api — Ordering API"

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

hadron: module "srv:qqq" does not exist — create it first with --new-module

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 extract splits 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-source also 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 with spec new <loc> and trim the source with spec edit.
  • spec link cross-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 replace does a citation-aware find/replace across every spec's body and abstract. It is word-boundary-aware by default, so renaming h-read-node never touches h-read-nodes. Preview with --dry-run, cap the blast radius with --max-specs N, and pass --yes non-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):

hadron spec supersede api:010:04 -m acme.com:specs \
  --title "Some new spec" --copy-body --yes

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.