Skip to content

Maintain product specs

CLI onlyAdvanced~20 min

This guide is for the person who writes and curates specs: capturing a durable product rule as a spec node, and keeping the corpus correct over time. If you only need to find and read a spec, see Read and cite product specs. If you own the rules but would rather a coding agent drive these commands, see Manage product specs with a coding agent.

hadron spec is the opinionated layer over node/edge: it allocates the next citation, scaffolds the required rubric, wires the edges, and enforces the stability rules so a citation is a permanent handle. The full command surface is in the hadron CLI reference.

Prerequisites

  • The hadron CLI installed and signed in — see Install the hadron CLI. Check with hadron auth status.
  • Write access to a memory that follows the loc-as-citation convention. The flat examples below use acme.com:specs; the product-rooted ones use acme.com:platform-specs.
  • Familiarity with how a citation is structured — Read a citation covers the two schemes and the tier 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.

Check (or declare) the scheme

Most single-product memories are flat (<module>:<feature>:<rule>) and need no setup — skip ahead. Use the product-rooted scheme (<product>:<module>:<feature>:<rule>) when one memory spans several products, so each citation names its product.

spec describe reports which scheme a memory uses, the products and modules present, and the per-tier counts:

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

The scheme is derived from the live specs, and a memory can also declare it in its data — handy for an empty memory that wants to state its arity up front, after which describe flags any drift. Declare it once, before the first spec:

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

A memory should be all-flat or all-product; spec lint warns if it mixes the two.

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.

Pick the module (a frozen three-letter code, e.g. api for messaging) — and, in a product-rooted corpus, the product (the shippable artifact, 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 derived from the live nodes, including the next free feature per module and next free rule per feature.

Scaffold a spec into an existing feature

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

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.

Every root also gets its contract

Creating a root — with --new-path, or with --new-product / --new-module / --new-featurealso 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 — a tier without its contract is the case the retrofit below exists to clean up.

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 in a product-rooted corpus; the citations simply carry the product (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 every sibling at a tier — every rule of a feature, every feature of a module, or every module of a product — put it in that tier's general-provisions contract instead of repeating it. Siblings inherit it automatically. 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.

Add a contract to a tier that already has siblings

Introducing a contract into a tier that already has siblings is a retroactive constraint: the moment api:000 exists, every sibling at that tier must inherit it — and the pre-existing ones carry no such edge. spec lint fails for each:

CITATION  SEVERITY  RULE              MESSAGE
api:010   error     inheritance-edge  no inheritance edge to general-provisions contract api:000
api:020   error     inheritance-edge  no inheritance edge to general-provisions contract api:000
api:030   error     inheritance-edge  no inheritance edge to general-provisions contract api:000

The remedy isn't in the message: back-wire each sibling to the contract with the inheritance edge spec new would have added. Use hadron edge add (or the hadron_create_edge MCP tool), with the name inherits the shared contract (general provisions):

for f in api:010 api:020 api:030; do
  hadron edge add --from "hrn:node:acme.com:specs:$f" --to hrn:node:acme.com:specs:api:000 \
    --name "inherits the shared contract (general provisions)"
done

Re-lint to confirm the tier is clean:

hadron spec lint --module api -m acme.com:specs   # ✓ api — all specs OK

The same retrofit applies at all three tiers — feature siblings back-wired to a module's :000, or module contracts to a product's :gen — not just the module tier shown here.

An outlier sibling still inherits

The rule is all-or-nothing per tier, which can feel forced when a tier 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. That feature still needs the edge. Read the inheritance edge as "governed by the tier's general provisions," not "is another instance of the same kind." Keep each contract broad enough to genuinely cover every sibling; an edge on a true outlier means only that it is bound by the shared provisions.

Lint it

spec lint checks a spec against the rubric and the stability rules. A freshly scaffolded spec 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 — a missing abstract, no "what invalidates" statement, a malformed citation, a name that doesn't lead with the citation, or a missing inheritance edge to a tier's general-provisions contract — exit with code 5. Warnings (a missing data.version, an absent edge, a mixed-arity corpus) do not, unless you pass --strict. Fix the findings and re-run until it is clean.

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.
  • 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 rule, supersede it: spec mints the next free number, 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:

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

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