Tutorial ยท about twenty minutes

Your first governed corpus.

An empty directory reaches a passing headwater check --strict in sixteen steps, and you leave holding shelf, kind, facet, overlay and obligation. Every command below was run, every output block is what that run printed, and the date of the run is 2026-09-09.

Before you start

You need four things.

  • git, and a name and an email address configured in it.
  • Linux on x86_64, or macOS on Apple silicon. You do not need a Rust toolchain.
  • curl and tar, to install the engine.
  • About twenty minutes.

Three facts about the blocks below.

  • A block is a command or it is output, and the two look the same. Every step gives the command first and what it printed after. Nothing marks the difference, so read the sentence above a block before you paste it.
  • Your dates differ. The engine reads a clock, and it puts the date of your run into what it writes. Where a block below shows 2026-09-09, yours shows the day you read this.
  • Long output is trimmed. A block that is shorter than the real output says so on the line above it.

Install the engine once. The block below downloads the release archive for Linux on x86_64 and puts the headwater binary in ~/.local/bin. The archive holds a static build, so it needs no particular C library.

mkdir -p ~/.local/bin
curl -fsSLO https://github.com/headwater-ai/headwater/releases/download/v0.5.0/headwater-v0.5.0-x86_64-unknown-linux-musl.tar.gz
tar -xzf headwater-v0.5.0-x86_64-unknown-linux-musl.tar.gz -C ~/.local/bin headwater

On macOS on Apple silicon, the archive is headwater-v0.5.0-aarch64-apple-darwin.tar.gz. Use that name in the curl line and in the tar line.

Each archive has a checksum file beside it on the release page. The name of the checksum file is the name of the archive with .sha256 added. To verify the archive, download that file too and give it to sha256sum -c, or to shasum -a 256 -c on macOS.

Check headwater --version prints a number.

Many Linux distributions put ~/.local/bin on your PATH when the directory exists at login. macOS does not. If your shell cannot find headwater, add ~/.local/bin to your PATH in the startup file of your shell. Then open a new shell. You can also run the binary as ~/.local/bin/headwater.

This installs version 0.5.0 of the engine. It does not install taxonomy-source, and step 3 fetches that. If you have a Rust toolchain, cargo install headwater-cli is an alternative route, and the README's Obtaining a named version section describes it.

01Make a repository that nothing describes

A repository with documentation in it, and nothing that says what the documentation is. Every adopter starts here.

Check ls docs/decisions prints one line, postgres-note.md.

mkdir -p ~/headwater-tutorial/docs/decisions
cd ~/headwater-tutorial
git init
printf '# Store attempts in Postgres\n\nThe queue keeps every delivery attempt in Postgres.\n' > docs/decisions/postgres-note.md

02Ask the engine what it can read off the tree

The first heading names what a tree states about itself: the corpus is docs, because that directory holds the most Markdown. The second names what no tree states, and those three questions are the interview. The verb asks about your documents, and never about the model โ€” the difference spec 7 draws.

Check ls .headwater prints overlay.yml and taxonomy.yml.

headwater init

what that prints

wrote .headwater/taxonomy.yml
wrote .headwater/overlay.yml

what this read off the tree
  corpus root docs
  package headwater/standard is not under `.headwater/packages/`. Two routes reach a lock, and each one needs a different field of `.headwater/taxonomy.yml`. Copy a package directory into `.headwater/packages/`, and pin `taxonomy.version` at the version that package declares. Or run `headwater taxonomy vendor <dir-or-location>` on a published artifact, unpacked or at the `https://` location of its zip: that verb reads `taxonomy.digest` and refuses until it holds the digest the publisher printed, and `headwater taxonomy resolve` reads `taxonomy.version` after it, so the vendor route needs the digest first and the version as well

what it cannot read off a tree, and asked instead
  the phrases each purpose answers, which decide what a task routes to
  the identifier scheme, and what its prefix discriminates
  which bundles this corpus already follows

Answer them in .headwater/overlay.yml, then run `headwater taxonomy resolve` and `headwater infer`

03Fetch the package into your tree

headwater taxonomy vendor is the vendor route that step 2 named. Give it the https:// location of a published zip and the digest that its publisher printed. It fetches the zip, checks every file against the digest, and installs the result. The command below fetches version 4.13.0 of headwater/standard from its release page.

The last line says that vendor wrote the digest you passed into .headwater/taxonomy.yml, in place of the commented # digest: line that step 2 wrote. It writes it only after the files match it, and only where the file pins no digest yet.

The from line is the location that vendor fetched. The digest line says that the files are the bytes the publisher pinned. .headwater/packages/headwater-standard is where vendor installed the package in your tree (HW-DR-0067).

A package carries a taxonomy: the kinds, the facets, the shelves and the rules. headwater/standard is the base package, and the taxonomy it declares is deliberately small. Four of the seven entries are not taxonomy at all. assemblies/ holds the publisher recipes this package ships. bundles/ holds optional traditions nothing here selects. doctrine/ holds the prose that explains them to a person. release.yml is the publish record that carries the digest this fetch just checked. Nothing you run in this tutorial reads any of the four.

Step 2 named two routes, and this step took the second. headwater taxonomy vendor installs a published artifact, which is what headwater taxonomy publish writes. This fetch just checked it, file by file, against the digest you passed. vendor fetched the zip itself, and after the install this is the one command in this tutorial that uses the network. Your organization can refuse a tool that reaches the network. Then get the zip by a means it allows, such as a mirror or an air-gapped copy. Unpack it, and give vendor that directory in place of the location. The digest check is the same for the two forms.

taxonomy/headwater-standard/v<version> is the taxonomy-only route. It publishes headwater/standard alone, with no engine release. A release workflow of this repository cuts a tag of this form whenever the package authors choose to. It needs no new engine version.

A second route pins a fixed version of the engine and of this package. The README's Obtaining a named version section names both. This step already pinned the digest, and step 5 pins the version this pulled, 4.13.0. Step 16 reads back what the second pin buys.

Check ls .headwater/packages/headwater-standard prints seven lines:

headwater taxonomy vendor https://github.com/headwater-ai/headwater/releases/download/taxonomy/headwater-standard/v4.13.0/headwater-standard-4.13.0.zip --expect sha256:b0f032403027e4c003396172c6ca3767709dcefa64317cbfc18dfa253adfb2a3

what that prints

vendored headwater/standard 4.13.0
  from https://github.com/headwater-ai/headwater/releases/download/taxonomy/headwater-standard/v4.13.0/headwater-standard-4.13.0.zip
  37 files, all of them the pinned bytes
  digest sha256:b0f032403027e4c003396172c6ca3767709dcefa64317cbfc18dfa253adfb2a3
  doctrine at .headwater/packages/headwater-standard/doctrine/
  pinned taxonomy.digest in .headwater/taxonomy.yml

Output trimmed to the lines that matter โ€” the full run prints more.

what that prints

assemblies
bundles
conformance.yml
doctrine
package.yml
release.yml
taxonomy.yml

04Meet the first refusal

A refusal here is the design and not a fault. A lock is a validated taxonomy, so a taxonomy that does not validate produces no lock at all. headwater init wrote version: 0.0.0, because it had no package in front of it to read a number from.

Check echo $? prints 1.

headwater taxonomy resolve

what that prints

headwater: the taxonomy did not resolve, so no lock is possible
  .headwater/taxonomy.yml: this takes headwater/standard 0.0.0, and the package here is 4.13.0

05Pin the version, and meet the second refusal

Open .headwater/taxonomy.yml. Change version: 0.0.0 to version: 4.13.0. The digest line above it is the one step 3 wrote.

Now resolve again.

The message names the second thing a package cannot know. A package that shipped a namespace would give every adopter the same one, so it ships none and refuses until you supply yours. The digest plays no part in this refusal. headwater taxonomy resolve reads the version and never the digest, so the pin that step 3 wrote changes nothing about what you see next. It matters starting at step 16.

Check grep -E 'digest:|version:' .headwater/taxonomy.yml prints two lines: echo $? prints 1.

what that prints

  digest: sha256:b0f032403027e4c003396172c6ca3767709dcefa64317cbfc18dfa253adfb2a3
  version: 4.13.0
headwater taxonomy resolve

what that prints

headwater: the taxonomy does not validate, so no lock is written. A lock is a validated taxonomy or it is nothing
  the resolved taxonomy `identifier_schemes.decision_id`: identifier integrity: carries no namespace after resolution. A package leaves the namespace to the corpus that adopts it, so an overlay of this corpus has to declare one

06Answer the question in the overlay

An overlay is what your repository says on top of the package it takes. Open .headwater/overlay.yml, and replace the last line, add: {}, with these two lines:

The overlay answers the question the refusal asked. Resolve again.

The lock is the one file every later command reads. An edit to the package or to the overlay reaches nothing until you resolve again, and that is the trap that catches most newcomers.

Check tail -2 .headwater/overlay.yml prints those two lines back. ls .headwater/taxonomy.lock prints the path.

the lines to write

add:
  identifier_schemes.decision_id.namespace: ACME
headwater taxonomy resolve

what that prints

wrote .headwater/taxonomy.lock
  from .headwater/packages/headwater-standard/taxonomy.yml
  from .headwater/overlay.yml
  no lock was there, so there was no adoption block to carry

07Check a corpus that nobody has typed

The most important state in the tutorial. The run is green, and it is green because it checked nothing: one file seen, none classified, none checked. A green run over an untyped corpus measures your taxonomy rather than your documents. Read the census before the verdict, on every run, forever.

Check headwater check --strict > /dev/null 2>&1; echo $? prints 0.

headwater check

what that prints

census
  1 files under the corpus root
        1 untyped

  docs/decisions/postgres-note.md
    untyped: no front matter, so nobody has typed this file. It sits on the
      `decisions` shelf, which types every file `decision`. Run `headwater new
      decision --title "<title>"` instead
  1 seen, 0 classified, 0 checked, 7 check instances
  0 findings

Output trimmed to the lines that matter โ€” the full run prints more.

08Type the document

Delete the note, and let the engine write the document in its place.

Three words of the model arrive in that one block, and the verb names each one as it uses it.

A shelf is a region of the tree that carries a purpose, and docs/decisions/** is the shelf named decisions. Placement is the loudest signal a document sends, so a shelf decides first and metadata never contradicts it.

A kind is what a document permanently is. The decisions shelf holds one kind, so placement alone settled that this file is a decision. A kind names the facets a document must declare, the sections it must carry, and the relations it may declare.

A facet is a property of one document, declared in the front matter. status, status_since, summary and last_verified are the four that decision requires. Three of them were filled by a role rather than by a guess. status came from the lifecycle regime, and two dates came from the run's clock. summary carries no role of its own. --summary <text> states it directly, exactly as --title states the name. Left unstated, the field carries a prompt for a person to answer.

The provenance block states what stands behind the document. The verb writes warrant: asserted in it, because nobody has accepted the decision yet.

Check cat docs/decisions/0001-store-attempts-in-postgres.md prints this, with your own dates:

rm docs/decisions/postgres-note.md
headwater new decision --title "Store attempts in Postgres" --summary "The queue keeps every delivery attempt in Postgres."

what that prints

wrote docs/decisions/0001-store-attempts-in-postgres.md

what the taxonomy decided
  kind decision on the shelf `decisions`
  identifier ACME-DR-0001 under `decision_id`, allocation reconcile-first
  status โ€” `regimes.lifecycle.standard` opens at `draft`
  status_since โ€” the facet is in the `state_entered` role, and the run's clock is the date
  summary โ€” the facet is in the `scent` role, and `--summary` is the sentence
  last_verified โ€” the facet is in the `freshness` role, and the run's clock is the date
  section `Context` โ€” the kind requires it
  section `Decision` โ€” the kind requires it
  section `Consequences` โ€” the kind requires it

Output trimmed to the lines that matter โ€” the full run prints more.

what that prints

---
id: ACME-DR-0001
status: draft
status_since: 2026-09-09
summary: "The queue keeps every delivery attempt in Postgres."
last_verified: 2026-09-09
provenance:
  warrant: asserted
---

# Store attempts in Postgres

## Context

TODO write this section.

## Decision

TODO write this section.

## Consequences

TODO write this section.

09Check again, and read the difference

The corpus did not grow. The count of checks that ran went from 7 to 21, because a typed document is a document that rules can reach. That is the whole trade this system asks for: type a document, and fourteen more questions become answerable about it.

Check headwater check 2>/dev/null | grep 'check instances' prints the second block above.

headwater check

what that prints

census
  1 files under the corpus root
        1 typed
        1 typed decision
  1 seen, 1 classified, 1 checked, 21 check instances

Output trimmed to the lines that matter โ€” the full run prints more.

10Commit the first governed corpus

Git refuses a commit from an author it cannot name. git config user.name prints yours. If it prints nothing, run git config --global user.name "Your Name" and then git config --global user.email "you@example.com" with your own values.

The file under ids/ is the claim on the identifier that headwater new minted. decision_id allocates reconcile-first, so the verb reads the tree for the highest value already spent. A tree tells it nothing about the branch somebody else holds. The claim file says the number is taken, and it holds the path of the document that took it. Two branches that mint one number add one path with two different contents, so the merge refuses and names both documents. Commit it with the document, and never write over one.

.headwater/capture-cost.jsonl is the one that takes a decision rather than a rule. It holds one reading per document that headwater new wrote, and it names no person and no agent. Headwater's own repository commits it, so that headwater capture can trend it. Keep it or ignore it, and know that you chose.

Check git log --oneline prints one line that ends in A first governed corpus. git ls-files .headwater ':!.headwater/packages' prints six files: the five you have already met, and the .gitignore that headwater check writes inside .headwater/cache/. The exclusion leaves out the package you vendored at step 3, which is a publisher's artifact rather than a file you wrote, and which ls .headwater/packages/headwater-standard already showed you.

git add -A
git commit -m "A first governed corpus"

what that prints

.headwater/cache/.gitignore
.headwater/capture-cost.jsonl
.headwater/ids/decision_id/ACME-DR-0001
.headwater/overlay.yml
.headwater/taxonomy.lock
.headwater/taxonomy.yml

11Declare an edge between two documents

A relation is a typed edge between two documents, and it names its target by identifier rather than by path. A path dies at the first rename and an identifier does not. supersedes requires both ends. The new decision is a draft, and nothing may rely on a draft yet, so the verb left the target alone. The far half is owed only once the new decision leaves draft.

Check headwater check --strict > /dev/null 2>&1; echo $? prints 0.

headwater new decision --title "Deliver at least once" --relates supersedes=ACME-DR-0001 --summary "Retrying a delivery is safe, so the queue may send one attempt twice."
wrote docs/decisions/0002-deliver-at-least-once.md

what that prints

the edges it proposed
  supersedes ACME-DR-0001 โ€” `created_by: scaffold`, so a scaffold pays for it
    the far half `superseded_by` is owed by docs/decisions/0001-store-attempts-in-postgres.md once this document leaves `draft`, and `headwater check --fix` writes it then

Output trimmed to the lines that matter โ€” the full run prints more.

12Promote the new decision, and read the finding

Open docs/decisions/0002-deliver-at-least-once.md. Change status: draft to status: current. A document states the state it will hold once your change lands, so a decision you propose as settled is current.

The new decision is live now, so the far half is owed. One edge has one end.

Read the identifier in parentheses. An obligation is a claim this system makes about itself, with the rule that verifies it named beside it. OB-REL-1 is the obligation that this rule discharges, and every finding names the one it serves.

One of those readings is about the package and one is about your run. The obligations and their severities come from headwater/standard. They read the same on your first day and on your thousandth. The last line is derived from the run in front of you. It names every rule that fired at you with no obligation behind it. Most rules carry one, which is what makes the identifier in your finding worth reading.

One rule is named there. facet.value.blank reports a facet a document declares and leaves empty, and no control in headwater/standard names it yet. HW-OBL-0170 records that debt. A finding it raises is a true finding, and the line above is how a report tells you which of its rules answers to nothing.

Your corpus will not show you a gap, and the reason is worth knowing. A gap is a disposition that a package author writes, with an owner, for an obligation that no mechanism verifies. The base package declares none, so this row reads 0 gap on every run of yours and no step here moves it. Headwater's own corpus takes a bundle that declares obligations that no mechanism verifies. The same block there shows gap and unverifiable counts above zero, and it names the owner of each gap. To read the current counts, run headwater check 2>/dev/null | grep 'obligations:' in a clone of Headwater. The number worth watching is the one that is not verified.

The word mechanical on the fix line is the second thing to read. A rule is an error when the repair takes no judgment, and advisory when the repair is a rewrite. This one takes no judgment, so the next step is a command rather than an edit.

Check headwater check --strict > /dev/null 2>&1; echo $? prints 1. headwater check 2>/dev/null | grep 'obligations:' prints the 37 obligations: line of the register block above, and nothing else.

headwater check

what that prints

  1 findings
        1 โœ— error

  docs/decisions/0002-deliver-at-least-once.md:11:7 โœ— error
    relation.reciprocity.missing (OB-REL-1): `ACME-DR-0002` declares
      `supersedes: ACME-DR-0001`, and `supersedes` requires both ends, so
      docs/decisions/0001-store-attempts-in-postgres.md owes `superseded_by`
    fix (mechanical): add `superseded_by: ACME-DR-0002` under `relations:` in
      docs/decisions/0001-store-attempts-in-postgres.md

what that prints

  register
    37 obligations: 37 verified, 0 gap, 0 unverifiable, 0 with no disposition
       13 high, 13 verified
       19 medium, 19 verified
        5 low, 5 verified
    facet.value.blank reaches no obligation, so it names none

Output trimmed to the lines that matter โ€” the full run prints more.

13Let the engine make the repair

--fix writes only the repairs that need no judgment, and it leaves every finding whose remedy is a rewrite. Read the diff before you commit it.

Check headwater check --strict > /dev/null 2>&1; echo $? prints 0, and grep -A2 '^relations:' docs/decisions/0001-store-attempts-in-postgres.md prints:

headwater check --fix
headwater: fixed docs/decisions/0001-store-attempts-in-postgres.md (1 patch)

Output trimmed to the lines that matter โ€” the full run prints more.

what that prints

relations:
  superseded_by:
    - ACME-DR-0002

14Ask the engine what it knows about one document

Every word this tutorial taught is on that screen at once. There you read the shelf, the kind, the facets, the sections, the relations and both halves of the edge. This is the verb to reach for before you open a file.

Check The first line of that output is the path of the document whose identifier you named.

headwater explain ACME-DR-0002

what that prints

docs/decisions/0002-deliver-at-least-once.md
  ACME-DR-0002
  kind decision
    shelf `decisions` matched on `docs/decisions/**`
    `decisions` is homogeneous, so placement carries the kind `decision`
  purpose rationale, to explain why a choice was made and what it forecloses
  summary Retrying a delivery is safe, so the queue may send one attempt twice.
  warrant asserted
  requires the facets status, status_since, summary, last_verified
  requires the sections Context, Decision, Consequences
  may declare supersedes to governed_document, and the other end writes
    superseded_by
  may declare governs to code_path
  may declare constrains to decision
  may declare conflicts_with to decision
  may declare traces_to to governed_document, code_path
  to ACME-DR-0001 supersedes โ€” The queue keeps every delivery attempt in
    Postgres. (the target's own summary) [this document governs the reading]
  from docs/decisions/0001-store-attempts-in-postgres.md superseded_by โ€” The
    queue keeps every delivery attempt in Postgres. (the target's own summary)
    [this document governs the reading]

15Ask a question in your own words

A purpose is the reader intent that a kind exists to serve. The base package declares two, rationale and behavior, and the route scored your question against both before it looked at any prose. That is why the summary facet deserves the most care in any document. Routing serves it as the only pointer a reader gets.

The pointer ends in [asserted: nobody accepted this document], because the decision still states the warrant that headwater new wrote. When a person accepts the decision, they change warrant: asserted to warrant: accepted and add their name in accepted_by. Then the mark goes away.

Check The pointer line names docs/decisions/0001-store-attempts-in-postgres.md. The line under it names the words of your question that reached that document.

headwater route "why do we keep attempts in postgres"

what that prints

route "why do we keep attempts in postgres"
  terms why do we keep attempts in postgres
  distinctive why do we keep attempts in postgres
  purpose behavior 3
  purpose rationale 3
  docs/decisions/0001-store-attempts-in-postgres.md โ€” The queue keeps every
    delivery attempt in Postgres. [asserted: nobody accepted this document]
    matched attempts in postgres, rank 1 of 1

16Ask what you have not wired up

Your corpus already stands on the first two rungs, which a package copied by hand never reaches. pin.current is what carried you there. It reads the digest step 3 pinned against the record .headwater/packages/headwater-standard/release.yml carries, and step 3 already gave you both. A package you copy from a source directory has no digest to pin, because nobody has published it. That route never reaches this rung: it stays off the ladder for the whole life of a corpus built that way. One gap is left, and it closes with a command:

headwater generate wrote the two projections that were missing, docs/decisions/README.md and .headwater/corpus.json, and that closed projections.current, the last rule L2 asks for. Every rung this ladder has is reached. A level still measures what you wired up rather than what your documents say. Climbing it here took one pin and two commands, not a better decision record.

Check The last line of the levels block reads L1 reached, against headwater/standard 4.13.0. headwater conformance 2>/dev/null | grep 'L2' prints L2 Regenerated โ€” reached, 4 of 4 rules met and, further down, L2 reached, against headwater/standard 4.13.0.

headwater conformance

what that prints

levels
  L0 Pointed at โ€” reached, 2 of 2 rules met
  L1 Classified โ€” reached, 3 of 3 rules met
  L2 Regenerated โ€” not reached, 3 of 4 rules met
    1 gap, 0 of them waived

L1 reached, against headwater/standard 4.13.0
  a level states what this repository wired up. It measures nothing about the
  corpus, no key declares one, and a waiver moves the exit status and never the
  level.

Output trimmed to the lines that matter โ€” the full run prints more.

headwater generate
headwater conformance

The words this teaches

Say each of these back before you leave. A finding you meet later names two or three of them in one line.

Corpus. The tree of documents the engine walks. Everything outside it is invisible to every rule.
Shelf. A named region of the corpus, and the primary classification axis, because placement is the loudest signal a document sends.
Kind. What a document permanently is. decision is a kind. A draft decision is not, because a draft is a state and not a species.
Facet. A property of one document, declared in the front matter. A facet that no rule and no projection reads is refused as unread.
Overlay. What your repository declares on top of the package it takes: your namespace, your phrases, your extra kinds.
Obligation. A claim this system makes about itself, with the rule that verifies it named beside it.
Package. A versioned bundle of taxonomy that somebody publishes and you take.
Lock. The resolved taxonomy, and the only file the checks read.
Census. The account of every file under the corpus root, which says how many were typed and how many were checked.
Relation. A typed edge that names its target by identifier, and that may require both ends.

Where to go next

Point the engine at your own repository. The steps above used a corpus made for the purpose. A tutorial cannot state what you should now see about a tree it has never read. Your own tree is the one that answers whether this is worth adopting. Run headwater init in it, fetch the package as step 3 did, resolve, and then run this:

It reports the files that classify as nothing, and the documents that state no summary. Those two lists are the distance between your repository and a corpus. headwater infer --owner <name> --write records that distance as declared debt with an expiry, so a strict run passes while the work is outstanding.

headwater infer

Read headwater check next. The verb contract states what it reads, what goes to each of its two streams, and the twelve causes behind its one non-zero exit.

This corpus reached L2. Step 16 climbed the whole ladder, from an untyped file to every rule this base package states about itself. A level names what a repository wired up, never what its documents say. Step 16 said the same, and it is the last thing this page has for you.