The same fact lived in nine hand-kept places and they disagreed about 58
proposals. This makes the file the only authority and everything else a
projection of it.
ONE VOCABULARY, in vocabulary.json, read by the lint, the generator and the
doc site. Three words, each answering "can I rely on this?" and nothing else:
Draft nothing implements it yet, or its own text says the work is ahead
Final the thing it specifies exists in the code
Living governance and process: amended in place, never finished
Eight spellings folded in. `Active` was the one doing real damage: on four Meta
and Process HIPs it meant living, on 48 others it meant shipped, and one word
doing two jobs is why nothing agreed. `Review` and `Last Call` described a
queue rather than a document, and a proposal under review is still one you
cannot build against.
STATUS IS EARNED, not declared. Every Standards Track proposal was checked
against the estate: 235 have their implementation, and eight do not and say so.
HIP-0095 carries a twelve-week plan ending at "mainnet launch" and has no code.
HIP-0525 specifies an all-machine meeting while apps/meet refuses a machine a
seat. HIP-0078 names a prover at a "planned location" that does not exist.
HIP-0114 is the cog.* procedure set, and that procedure set returns nothing.
HIP-0003, HIP-0102, HIP-0305 and HIP-0901 likewise: looked, found nothing.
SIXTEEN TOMBSTONES DELETED rather than statused. A dead document kept alive in
the index is one concept wearing two names. Each successor exists and carries
the text, and the 90 references that pointed at a tombstone now point at it --
whole markdown links at once, so link text cannot outlive its target. Three of
the sixteen were pure compatibility: HIP-0028 specified a migration off a
Bitnami chart, HIP-0029 dual-engine MySQL, HIP-0030 a schema-deprecation
policy. We ship no backwards compatibility, so we do not specify it.
Nine links were already broken and had been since the corpus was renumbered:
short-form ./hip-5.md paths that never existed. Two named the wrong proposal --
one cited "HIP-1: Hanzo Multimodal Models" where multimodal is HIP-0003.
THE GENERATOR REFUSES more often than it writes, because a short table looks
exactly as authoritative as a complete one. Rows must equal files; a shrinking
index must say by how much; a section whose input is missing is an error, not
an omission; nothing is written until all of it validates. Its self-test breaks
the corpus eleven ways and requires each to refuse AND leave README byte-
identical -- a guard that reports after half-writing has protected nothing.
The lint's second implementation of the same comparison is gone.
The site rendered 52 proposals with a fallback grey badge and counted them
toward none of its three stat tiles, because docs/lib/source.ts declared a
status union by hand: it admitted a `Stagnant` no file has ever carried and
omitted the one 52 files did. Five components each kept their own copy of the
colours. All six now read vocabulary.json.
9.1 KiB
HIPs
Hanzo Improvement Proposals: the specs in HIPs/, indexed by README.md, and
the site at https://hips.hanzo.ai built from docs/.
How this ships
One way, and it runs on our own stack:
push -> github.com/hanzoai/HIPs (development lands here)
-> git.hanzo.ai/hanzoai/hips CANONICAL
.hanzo/workflows/sync-from-github.yml PULLS main, every 10 min
.hanzo/workflows/ci.yml checks the index
.hanzo/workflows/deploy.yml builds + publishes the site
-> api.hanzo.ai/v1/projects/hips/deploy the Sites plane
-> hips.hanzo.ai
git.hanzo.ai is canonical; GitHub is where people push. Every check and
deploy is a workflow under .hanzo/workflows/, which the forge reads.
.hanzo/workflows uses GitHub Actions syntax, so a workflow moves between the
two by changing directory and nothing else.
The forge takes the FIRST workflow directory that exists, so .hanzo/workflows
also shadows .github/workflows there — mirrored GitHub workflows cannot execute
on the forge by accident.
The sync is a PULL, and that direction is load-bearing. It used to be a push
from .github/workflows/sync.yml; 3047c73 deleted that on the belief the Hanzo
GitHub App webhook had superseded it. No such webhook exists on this repo, so the
forge froze at 9f792bf while GitHub moved on 15 commits, deploy.yml never
reached a runner, and the site stopped publishing for a day without failing
anything. A pull cannot fail that way: it needs no credential (this repo is
public), no webhook, and no GitHub runner — and hanzoai currently has zero
online GitHub runners, which is why the old push job queued for hours and never
ran. Fast-forward only; a divergence fails loudly rather than force-pushing.
No GitHub Pages and no Cloudflare Pages. The site is static files the Sites plane serves — no image, no CR, no pods, because nothing here executes.
What checks what
One lint, two callers. scripts/lint-hips.py is the only implementation of
the rules, and both a contributor and CI run the same file:
python3 scripts/lint-hips.py # exits 1 on any ERROR
python3 scripts/lint-hips.py --list # the checks, by code
python3 scripts/test-lint-hips.py # proves the lint FAILS when it should
python3 scripts/index.py # project HIPs/ onto README
python3 scripts/index.py --check # exits 1 if README has drifted
python3 scripts/test-index.py # proves the generator REFUSES
.hanzo/workflows/ci.yml used to carry its own copy of the checks as an inline
heredoc. That meant the gate existed only on the forge and a failure could not be
reproduced without pushing. It now calls the script, and calls the self-test
first.
The self-test copies the corpus, breaks it 20 ways — one per check — and requires the matching error code each time. It also asserts that every check the lint declares has a test, so adding a check without one fails. This matters here more than most places: the GitHub workflow this lineage replaced printed every missing field and then exited 0, so a HIP with no status shipped green. A check that has only been seen to pass has not been shown to work.
Checks are FM* front matter, ST* structure, LK* links, IX* numbering,
PL* policy. Each
fired on a real defect when it was written: 101 H1 headings disagreeing with their
own front matter, 16 files with no H1 at all, 9 files using a status word outside
the vocabulary, a requires: pointing at a HIP that never existed, 5 duplicate
headings, and 4 subsections numbered outside their parent section.
Two policy checks are worth knowing about because both were wrong on the first attempt and had to be narrowed:
- PL001 fires only when a comparative phrase sits within 80 characters of a named third-party product. Firing on the phrase alone reported 15 lines of which 12 were ordinary English ("only as good as the source behind it"). It deliberately does NOT fire on weighing two candidate upstream dependencies against each other — that is engineering rationale, not a claim about us.
- PL002 fires on a specific private-org repository path, not the bare org name. HIP-0135 is exempt: it is the document that draws the line, so it must be able to name both sides.
One index, generated
HIPs/*.md front matter is the only authority for what a HIP is. The site reads
../HIPs directly; README.md's two tables are projected by scripts/index.py.
The generator refuses far more often than it writes, because a short table
looks as authoritative as a complete one. Nothing is written until everything
validates: rows must equal files exactly, a table that shrinks must say by how
many (--delete N), a section that cannot be rebuilt is an error rather than an
omission, and every requires: must resolve. scripts/test-index.py breaks the
corpus eleven ways and requires each to both refuse AND leave README.md
byte-identical — the second half being the one that matters, since a guard that
reports after half-writing has protected nothing.
scripts/index.py --check regenerates in memory and compares, which is how CI
holds README to the files. The lint used to carry a second implementation of
that comparison (IX001, IX002, IX004: row-by-row title, type, category and
status); it is gone. The generator is the one statement of what the projection is.
The vocabulary lives in vocabulary.json, which the lint, the generator and
the doc site all read. It used to be written by hand in nine places — including a
TypeScript union in docs/lib/source.ts that admitted a Stagnant no proposal
has ever carried and omitted the one 52 proposals did, so every one of those
rendered with the fallback badge and counted toward none of the three stat tiles
on the home page.
Four committed hip-index.json files used to exist alongside it. They are gone.
docs-old/ — 16 tracked files including its own index — described 25 HIPs
while 138 existed, and claimed four (16, 21, 22, 23) that do not exist at all;
nothing referenced it. docs/hip-index.json and docs/site/hip-index.json were
generated-and-committed, already 3 behind, and read only by docs/site/index.html,
which is not in the deployed export. Do not reintroduce a committed index. If
one is needed, generate it during the build.
Deploying the site
deploy.yml builds the static export and publishes it to the Sites plane. There
is no image, no CR and no operator step: this is a Next.js export, ~170 MB of
files that never execute, and baking them into a container so a Go binary can
serve /public buys nothing.
pnpm build -> docs/out
-> POST /v1/projects/hips/deploy {"source":"git"} 202 + an upload grant
-> POST each file under the grant bytes skip the API
-> POST .../deployments/<id>/complete {status,keys}
This repo holds no S3 credential. It used to need the hanzo-sites bucket
keys — one shared bucket separated only by key prefix, so those keys were write
access to every org's site. The 202 now carries a presigned POST policy confined
to this site's prefix and expiring in 30 minutes. Deletion moved with it: CI
reports its manifest as keys on completion and cloud prunes the prefix, because
a write-only grant cannot delete.
The one secret is HANZO_DEPLOY_TOKEN, which identifies us rather than granting
storage. It is set on the forge, since .hanzo/workflows/ is what the forge
reads; GitHub's secret store is not in this path at all.
Writing a HIP
Copy docs/templates/hip-template.md to HIPs/hip-<NNNN>-<slug>.md, then run
python3 scripts/index.py and python3 scripts/lint-hips.py.
It lands as Draft. It becomes Final when the thing it specifies exists in the
code — that is the only thing that moves a status. Meta and Process HIPs are
Living: they are amended in place and never finish.
The template exists now. It did not until 2026-08-04, although this file had told every author to copy it for months — which is the simplest explanation for why 138 HIPs drifted into four H1 styles, eight status words and three different spellings of "Security Considerations".
One thing we build → one public repo → one HIP. A Standards Track HIP
describes something that exists; type: Process is for how we work and is not
expected to map to a repository.
Measured on 2026-08-04, the corpus is a long way from that rule and it is worth knowing before adding to it:
- 96 of 121 public
hanzoairepositories are named by no HIP. - 69 of 123 Standards Track HIPs name no
hanzoairepository at all. - 24 Standards Track HIPs specify only PRIVATE repositories — including
HIP-0106 (
cloud), HIP-0027 (kms), HIP-0068 (ingress) and the whole CRD family (operator). By HIP-0135's own rule a private repository inhanzoaiis mislabelled or in the wrong org, so the fix is to publish, not to withdraw the HIP. - Fragmentation: 19 HIPs describe
operator, 7 describecloud.
Before writing a new HIP, check whether the thing already has one. HIP-0041 was rewritten rather than duplicated for exactly this reason: a brief asserted no CLI HIP existed, and HIP-0041 had specified the CLI since February.