Commit Graph
6948 Commits
Author SHA1 Message Date
zooqueen 9bd4887a6a Remove the docs auth module
689 lines under radix-ui/src/auth: an OIDC login/callback/logout handler
set, a session cookie, JWT middleware and a page-tree access filter. It
imports jose, which the package does not depend on, so it has never
compiled. tsdown has no entry matching it, dist has never contained it,
the exports map does not name it, and the only importer is its own test.

It is also the wrong shape and the wrong home: sessions, tokens and gates
belong to IAM, and a documentation component library is not where an auth
middleware lives. Nothing consumes it, so nothing breaks.

Recover with: git checkout 955dd5989 -- packages/radix-ui/src/auth
2026-08-27 17:05:18 -07:00
zooqueen be06951f9c A separator's class can be a function of its state
Base UI passes component state to className when the caller gives it a
function. SelectSeparator handed that argument straight to cn(), which
takes class values, so a function className would have been stringified
into the class list. Takes the state form tabs.tsx already uses.
2026-08-27 17:05:18 -07:00
zooqueen 71d9d14041 i18n names the types core owns
src/i18n.tsx asked contexts/i18n for Translations, which contexts/i18n
imports from src/i18n -- a cycle, and the name is declared right below
the import. Meanwhile TranslationValue, TranslationsAPI and
TranslationsAPIExtension moved to @hanzo/docs-core/i18n and were left
unimported, so both packages had been referencing a vocabulary they no
longer named. Nine type errors in radix-ui, six in base-ui.
2026-08-27 17:05:18 -07:00
zooqueen e158f03c4d The registry declares only what the compiler reads
Both registry files carried the same de-branding leftover: Registry
imported twice, once from @hanzo/docs-cli/build and once from
fuma-cli/compiler. The first is `export * from` the second, so it was the
same type under the same name.

They also set env and onResolve. Neither is on the Registry interface and
fuma-cli's runtime never reads either -- zero references in its dist --
so both were inert. onResolve called resolveExternal, which nothing
exports; had anything reached that line it would have thrown. The two
copies disagreed about the argument anyway: base-ui's named docs-ui,
radix-ui's named base-ui, its own alternative rather than itself.

Removing them takes five type errors off each package and changes no
generated output.
2026-08-27 17:05:18 -07:00
zooqueen edf3387993 Delete the home client no route reaches
layouts/home/client.tsx names seven modules that do not exist --
parseLayoutProps, renderTitleNav, NavigationMenuRoot, three shared
toggles and utils/renderer -- and accounts for fifteen of base-ui's
twenty-seven type errors. It is not in the exports map, tsdown has no
entry for it, layouts/home/index.tsx does not import it, and radix-ui has
no counterpart. Nothing has ever built it.
2026-08-27 17:05:17 -07:00
zooqueen 3adfc568a8 Drop the react import nothing calls
useEffectEvent was named in eleven files that never call it. Ten were in
radix-ui alone, against a base-ui copy that upstream keeps byte-identical
to it, so an unused name was the entire difference between the two; the
eleventh, toc/index.tsx, carried it in both. Restoring the ten from
base-ui makes each pair identical again.

The symbol is real and React 19.2 exports it -- it is used, twice, in
thirteen other files. These eleven just never reached for it.
2026-08-27 17:05:17 -07:00
zooqueen 3d682b6fcd Each docs site renders on one adapter
The two UI packages are adapters over different headless primitive
libraries: base-ui wraps @base-ui/react, radix-ui wraps @radix-ui/*. They
are meant to be alternatives. Every app here had been pulling from both.

Each package builds its own React contexts, so a provider from one is
invisible to a consumer from the other. insights-docs, pulsar-docs and
zen-docs mounted RootProvider and the CSS preset from base-ui but took
DocsLayout and the MDX map from radix-ui, so the layout's search trigger
read radix's SearchContext default -- enabled:false, setOpenSearch a
no-op. SearchTrigger returns null when disabled, so the control was not
dead -- it was ABSENT. Measured on a built insights-docs: [data-search]
0 -> 1, the mobile header gaining a 34x34 search button. The keyboard
path still worked, because base-ui's SearchProvider was mounted and
registers its own keydown listener: Meta+K opened the dialog at base.
So the defect was an undiscoverable search with no affordance and no
kbd hint (hotKey: []), not a search that could not be reached.

apps/docs was consistent in its app code (layout, provider, MDX map, CSS
all base-ui) but 28 hand-written MDX pages and the two resolveAlias
entries reached for radix Callout, Card, Files and Tabs, which loaded the
whole second library alongside the first.

Tabs draws ONE frame before this and one after, and the earlier claim
that it drew two was wrong. quickstart.mdx already imported base-ui
tabs, and base-ui's tabs.tsx already carried the collapse logic -- a
file this change does not touch. The alias repoint removes a latent
trap for the next upstream sync; it fixes no observed defect.

Callout, Card and Files are byte-identical across the two packages, so
this moves no pixels.
Nothing in apps/ imports docs-ui now, and its dependency entry goes with
it. The registry generator still names both -- it publishes both.
2026-08-27 17:05:17 -07:00
zeekay 6d7bf10222 regenerate the reference and the project rail at cloud 733acc2f6d
The document moves to 1623 paths and 2269 operations, and the CLI table to
2249 commands, so every flow now resolves a CLI command where before four of
twelve did not.

The project rail is the larger correction. The manifest carried 70 projects
because the listing that built it ran out of quota; the org publishes 334, and
223 of them appear here for the first time. Ten leave, each for a reason: nine
went private since the last manifest and had no business on a public page, and
go-sdk was transferred to hanzo-go/sdk, so the listing no longer carries it and
only a redirect answers the old address.
2026-08-27 16:30:16 -07:00
zeekay ab03a56d35 ask GitHub with a credential, and prefer the App's
The project sync ran with no credential at all, which is sixty requests an
hour: it exhausted the quota partway through the READMEs and then wrote a
manifest from the listing it had managed to read -- 70 projects where the org
publishes 334.

One credential now, asked for by org, for every read here. A GitHub App
installation token is preferred where the App is configured, because the quota
it spends belongs to the installation rather than to a person; it is the same
App cloud's integrations product uses, named the same way, so there is one
spelling of it. Anything that goes wrong reaching the App falls back to the
configured token -- an App that cannot be asked is a reason to use the other
credential, not a reason to fail the build.
2026-08-27 16:25:19 -07:00
zeekay f2a9cbea55 Merge remote-tracking branch 'origin/main' 2026-08-27 08:17:33 -07:00
zeekay 2c1ed50279 chore(docgen): 3.1.2 — readFile takes its encoding, codeblock states its default 2026-08-27 08:16:15 -07:00
zooqueenandHanzo Dev 2d62f3146d docs pages get room between the last card and the footer
Measured at a 0px seam: the docs grid ended exactly where the ecosystem footer
began, so the prev/next cards sat flush against the columns with nothing
between them. 96px on desktop, 64 below md.

The room goes in this layout rather than in the footer, which is byte-identical
across every Hanzo property and shared with the home and landing routes, or on
the article, which would have put the space inside the scrolling column instead
of after it. apps/docs is the only docs app that renders a footer under its
docs layout, so this is every page that had the seam.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 12:27:49 -07:00
zeekay 7685d2b693 fix(brand): the tab shows our mark, not the framework we built on
zen-docs and pulsar-docs both shipped Apache Pulsar's purple mark, the astro
and react-router examples shipped their upstream's, so a reader with two of our
docs sites open saw another project in both tabs. They are @hanzo/logo's
favicon now — and the examples matter most, because a template someone copies
carries whatever brand it was scaffolded with into their project.
2026-08-25 11:58:23 -07:00
zeekay fd11bafcfa docs(examples): the templates people copy start on Zen, self-hosted
Twelve example apps opened with next/font/google Inter, so every developer who
copied one started their project on a face that is not ours and a request to a
font host we do not control. They use @hanzo/font's ZenSans (and ZenMono where
the example wanted a second family), with the dependency declared so it
resolves. examples/proposals bound --font-sans to var(--font-inter), a variable
nothing sets any more; it reads --font-zen-sans.
2026-08-25 11:00:52 -07:00
zeekay 22fb04777d fix(font): the vendored Zen faces named Geist in their name table
The filenames read Zen-*.woff2 and the glyphs are right; only the name
table said family='Geist', designer='Basement.studio, Andres Briganti,
Mateo Zaragoza', vendorURL=basement.studio. Nothing visual can see that —
a font picker, a local() match and a licence audit can.

Replaced with the corrected bytes from hanzoai/font, matched by glyph
content rather than by filename, so the drawing is provably the same face.
mirror/main-26c64bd
2026-08-24 18:14:18 -07:00
zeekay b5cb8db1fb docs: a 5xx is not a refusal either, and the wait should grow
hanzoai/ci took the same fix further and both corrections apply here. A 429 or a
5xx is the gateway or the throttle answering about ITSELF, not KMS answering
about the secret — treated as final, either one fails a release for a reason
that would be gone a second later. Only a real verdict (2xx, or a 4xx that is
not 429) ends the loop now.

The wait grows 0/5/15/45 rather than three flat threes, because the socket drop
this exists for comes in bursts and a fixed retry lands inside the same one.

The `|| code=000` placement carries a comment now. It belongs on the assignment,
never inside the substitution: `-w` already prints 000 when curl fails, so a
second one makes the variable "000000" — which matches no case, and silently
turns an outage into a refusal that never retries. That is the bug ci found in
its copy; this copy never had it, and the note is what keeps it that way.
2026-08-24 08:55:12 -07:00
zeekay 043e84cdfd docs: wait out a lost KMS answer, and keep failing on a refused one
With the step finally able to report itself, it said: the login succeeds and
reading deploy/PUBLISHABLE_KEY answers 000 — no HTTP response at all, not a
rejection. cloud serves KMS as a plugin over a unix socket and that socket drops
responses mid-write under load: 47 `kms.sock … broken pipe` in one hour on a pod
with 0 restarts, and kms is not alone (agents 48, sandbox 20). Run #351 got its
answer; #352 through #354 did not.

So the two calls are asked up to three times, and ONLY when the answer was lost.
A code that came back — 401, 403, 404 — is an answer, and is still fatal on the
first try. This waits out a dropped response; it does not retry a refusal into
a success.

The underlying socket is cloud's to fix. This stops a docs release from being
decided by which side of that race it lands on.
2026-08-24 02:13:07 -07:00
zeekay 4802b94760 docs: the KMS step can finally say what went wrong
Two diagnoses of a live outage produced `exitcode '22'` and nothing else. The
reason is in the step: `curl -sf` under `set -euo pipefail` turns a refusal into
exit 22, the pipeline carries it, `-e` ends the script, and the very next line —
the one that would name which call failed — never runs. Every error message it
carries is unreachable for the failure it describes.

It asks for the status now and prints it, per call, with what each code means:
401 on login is this forge's credential being rejected, 000 is the endpoint
unreachable from the build container, 403 on the read is a client that may not
see that path, 404 is a secret that is not there. Same fail-closed behaviour —
it still refuses to bake an image that would emit unattributed events.
2026-08-24 02:08:12 -07:00
zeekay 03d35ca787 docs: the links a reader clicks now go somewhere
Probed all 328 clickable external links against a 404 control. The 401s and 405s
are code-sample endpoints answering correctly to an unauthenticated GET; these
were the ones actually broken.

`curl -fsSL https://hanzo.ai/install | sh` — the first thing the CLI page tells
you to run, and it 404s. The installer is served at hanzo.sh, which is what the
landing page and the quickstart already say. Fixed in the generator, so it stays
fixed.

The ZAP bindings pointed at `zap-protocol`, an org whose repos do not exist, and
`@zap-protocol/zap`, a package never published. The org is `zap-proto` and its
repos are named for the language — `go`, `rust`, `py` — not `zap-go`. The Go
import path was wrong with it: proxy.golang.org serves `github.com/zap-proto/go`
and refuses `zap-protocol/zap-go`. 18 files.

Four binding pages listed example files that do not exist in any of those
repositories; they now link to what does. `luxfi/contracts` is private, so both
links 404 for every reader — the prose keeps the contracts and points at docs a
reader can open. `charts.hanzo.ai` does not resolve and `helm repo add` is not
how this estate ships charts: universe installs `oci://oci.hanzo.ai/charts`.
`unpkg.com/@hanzo/ast/browser` needs the file, `dist/browser.js` — unpkg does
not resolve export conditions.
2026-08-24 01:31:00 -07:00
zeekay ccd0633e5c docs: there are two desktop apps, and only one of them was on the page
hanzoai/desktop — agents built on this machine, against models running locally
or in the cloud — appeared nowhere. The page described hanzoai/extension's
menubar assistant and called it "Hanzo Desktop", which is also what the other
one is called, so a reader looking for the first found the second and no way to
tell. They are named here by what they DO.

Six download links pinned v1.9.31 and named the exact file. Both rot: the
version moves and the filename moves with it. Each download now points at that
project's `releases/latest`, which is stable, and the table says what to look
for rather than pretending to know the current name. Every link probed 200
against a 404 control.
2026-08-24 00:46:54 -07:00
zeekay 1374043198 docs: carry the @hanzo/ui bump into the lockfile
The manifest moved to ^8.3.3 and the lockfile stayed on 8.3.0, so every build
since has died at `pnpm install --frozen-lockfile` before compiling a line —
which CI sets by default, and which is the whole point of it. A version bump is
two files.
2026-08-23 18:07:34 -07:00
zeekay af364b12d1 docs: the collapsed rail's corner is one control, not a mark beside a button
A collapsed rail drew two things in the top-left: the rail's own control showing
a sidebar glyph, and the layout title beside it showing the mark — two targets,
one of which was not a target. They are one now. The mark is what is there at
rest, because it is what a reader recognises at that size; the glyph replaces it
under the pointer, which is the moment the control has something to say about
what it does. Both layers sit on the same spot and cross-fade, so the button
never changes size and the corner never reflows.

`sidebar.mark` is the node, supplied by the caller. Reaching for the `navTitle`
slot instead is the obvious shortcut and is wrong twice: it is a link to the
site root, so an anchor lands inside a button and wins the click — the control
would navigate home rather than open the rail — and it answers "what fits the
space", where the space here is always the collapsed one.

Measured in a browser across the whole cycle: collapsed, one mark on screen at
x=16..52 and the glyph at opacity 0; hovered, they swap; clicked, the rail
expands and the URL does not move. The nav keeps its rhythm either way — 32px
after an open rail, 36px after the collapsed control.
2026-08-23 18:04:18 -07:00
zeekay 76784b375a fix(docs): @hanzo/ui ^8.3.3 — the pin was exact and one patch short
8.3.0's theme.css inlined design 0.5.17, the last tokens that named Geist.
That is why this site loads five Zen faces and paints Geist: the faces
arrive, and an unlayered :root from the component library renames the role
out from under them.
2026-08-23 17:11:16 -07:00
zeekay ddf39f0126 Say endpoint in the deploy check and the capability taxonomy
Both call an address that accepts requests a door. The deploy step curls one to
see whether the publishable key resolves, and capabilities.yaml describes what
each capability answers on — it is the one editorial file in this repo, so its
wording is read by every generated page.

Prose and one temp filename. Both files still parse as YAML.

openapi-specs/{openapi.yaml,hips.json} still carry the old wording: they are
synced copies of hanzo-inc/cloud and hanzoai/hips at pinned versions, and both
sources are fixed. They clear on the next sync.
2026-08-23 16:23:34 -07:00
zeekay 36e1403137 docs: studio is out of the sidebar on purpose, and now says so where the gate reads
Every build since studio left the rail has failed at the route gate — "1
top-level sections the root meta.json does not name" — because removing it from
`pages` states the decision in a place the gate cannot read. The mechanism for
saying it already exists and already carries enso, games and proof-of-ai: the
`unlisted` block in the same file, one edit away from `pages`, so a reader of
the sidebar sees both.

The pages stay published and reachable — /docs/studio answers 200 and the
product grid links it. What changes is that the gate can tell a deliberate
absence from an orphan.
2026-08-23 16:08:28 -07:00
zeekay a3bd53d0f0 docs: the Guides destination is one word, and every language names its own package
The nav said Guides while the URL, the page title and the sidebar heading all
said Start, and the two guide folders a reader would look for under it sat
beside it instead. Now /docs/guides is the address, the title and the heading,
with migrate (40 pages) and integrations (17) nested inside it. 64 exact
redirect rules carry every old address.

The generator owns the flows and no longer wipes the directory it shares with
those folders, and its meta.json reads them off disk, so adding a guide folder
is a directory rather than an edit here.

On the landing page each code tab carries the install line for the package it
imports, so a reader copying the Python example installs hanzoai and not the
orchestration library that shares the name. Rust joins the row — the crate
exists and the tab did not. The Go install line named github.com/hanzoai/go,
which resolves to nothing; the module is go-sdk.
v0.1.3
2026-08-23 16:01:49 -07:00
zeekay 1771326bd0 Eight links pointed into repositories a reader cannot open
Probed every external link the authored tree carries, 164 of them against a
control that 404s: 154 reachable, and the rest all one shape — hanzo-js/iam,
hanzo-rs/sdk, hanzobot/ts and hanzoai/node are PRIVATE, so a reader following
them gets GitHub's 404. Same defect the project pages had, in prose this time.

The Rust page was the worst of it. It did not merely link a private repo, it told
readers to DEPEND on one:

    hanzo = { git = "https://github.com/hanzo-rs/sdk" }

while the crate is published — `hanzo` 0.2.0, "Hanzo — the Open AI Cloud SDK for
Rust", confirmed through the crates.io API and again through the sparse index.
It says `hanzo = "0.2"` now, which is both installable and shorter.

The IAM page's five links went into a private repo's own docs/ directory for
things this site already publishes: the IAM reference is 160 pages at
/docs/openapi/iam, authentication is /docs/api-keys, billing is /docs/credits.
The package link is @hanzo/iam 0.21.16 on npm, which a reader can install.

pdf.hanzo.ai is not a host: it answers exactly as an unknown subdomain does, so
the wildcard is replying, not a service.

Two links to hanzoai/node are LEFT as they are, in a page hidden from the rail.
That repo is private today and intended to be public; the link is right about
where the code will be, and rewriting it would only have to be undone.
2026-08-23 15:07:24 -07:00
zeekay eec7ae3478 The one link on the site that led nowhere now leads to the catalogue it promised
Checked every internal link in the authored tree: 18,879 of them, and exactly one
pointed at no page — /docs/models, named three times by the pricing generator as
"the live catalogue, every model id and its current rate".

The catalogue existed and was reachable from nothing. ModelsCatalog is registered
for MDX and used by no page in the tree; it reads api.hanzo.ai/v1/models in the
reader's browser, which answers 200 with 536 models right now.

So the page is the component plus what a reader needs around it: the id goes in
the `model` field, the request is otherwise identical on every surface, and a
rate belongs to the model rather than to the surface you reach it through.

No count in the prose. The table is fetched when the page loads, so it is the
catalogue the gateway is serving rather than a number written down when the page
was built and wrong by the next model.
2026-08-23 14:52:43 -07:00
zeekay c9097ce9d3 Redirects that pointed at pages we no longer publish now point at ones we do
Of 47 rules, 26 named a target that does not exist — the whole /docs/services/*
and /docs/skills/* families, plus /docs/getting-started and /docs/llm. They were
dead twice over: nothing read the file at all until the emitter landed, and more
than half of what it would have emitted led nowhere.

Resolved against what the tree actually serves, not against what the paths look
like: 24 repointed, mostly to the capability's own reference page — /kms to
/docs/openapi/kms, /docs/cron to /docs/openapi/tasks, /docs/apm to
/docs/openapi/o11y, /docs/analytics to /docs/openapi/event, which is where that
capability went when it was renamed.

/gpus and /gpu are DELETED rather than repointed. There is no GPU capability in
the published contract and no successor page, so any target would be a guess,
and the emitter's own rule is that moving a reader before telling them nothing
is there is worse than the 404 it replaces.
2026-08-23 13:36:11 -07:00
zeekay 3c7a7d6a03 feat(font): replace Geist with Zen 2026-08-23 13:33:43 -07:00
zeekay 132aa8e8fa Say MCP, endpoint or edge instead of door in the authored docs
The generators stopped emitting the metaphor in f1178b6b85. This is the rest of
it: the hand-written pages, the site's own components, and the two configs.

"door" named five different things, so each site now names its own: the MCP
server where it is POST /v1/mcp, endpoint where it is one address that accepts
requests, entry point where it is the single way in, the edge where a credential
is validated, address where it is a location. Three sentences that were only the
metaphor now state the fact instead.

/llms.txt was publishing it to every agent that reads the site.

docs-hero's DOORS list is renamed PATHS, which is what its own doc comment
already called it — three paths ordered by how much you type. It is local to
that file and exported nowhere, so nothing outside it moves.

MCP_DOOR, cat.door, doors(), type Door and the `doors:` key in capabilities.yaml
keep their names: renaming those is a cross-file change to a tracked schema, not
a prose edit. capabilities.ts keeps the one comment quoting pubsub's package doc,
because stripping it would leave a rationale that explains nothing.

Measured: authored pages 39 -> 1 (a smart-home camera nickname, a literal door),
scripts 151 -> 45, all 45 identifiers. docs-mdx exits 0; the two script test
files fail 5/30 identically with and without this change.
2026-08-23 13:31:24 -07:00
zeekay d2e1771698 Publish a glossary, and name things plainly in the generated reference
One idea had several names, so every reader paid a translation. The glossary
states one name per idea and gives the aliases other systems use, so a reader
arriving with a different word can find ours.

It is written against a census of what the corpus actually says, not against
recall: 416 prose pages, ranked by use. The words that carry the docs and were
never defined — caller, run, operation, capability, surface — are defined; the
general terms nothing outside a glossary would use are not.

The reference generators said "door" for three different things, and being
generated it reached 1,962 pages. They now name what they mean: MCP where it is
the MCP server, Address where it is a location. The nav label is where the link
goes rather than a slogan. Measured: door 1,962 -> 103 files, the label 2,373 ->
1, the Door column 121 -> 0. The last 103 carry cloud's own doc comments through
openapi.yaml and are fixed there.

`pnpm lint` could not run at all: the collections alias lived only in
next.config.ts, which rolldown never reads, so lint failed at module resolution
before reading a page — a check that never reported it was not running. The
alias is now one list, projected twice, because Turbopack resolves it relative
to the app root and rolldown demands an absolute path.
2026-08-23 13:14:55 -07:00
zeekay 1ef26e66c1 The CLI entry copies hanzo code, not hanzo dev
Pinning dev chose the reader's coding agent for them. The bare form runs
whichever they configured — dev, Claude Code and Codex are all first-class — so
the command names the task and leaves the agent to their settings.
2026-08-23 13:14:10 -07:00
zeekay d455ca3be0 llms.txt named 26 of its 35 sections by directory, and the agent menu was one flat list
The index every agent reads opened its sections with raw slugs — api-keys, apps,
chat, concepts, console, credits — because the heading came from a hand-kept map
of eight and everything absent from it printed its folder name. Every one of
those sections already carries a title: a folder's meta.json, or the page's own
frontmatter. It reads the tree the sidebar renders now, so a section is named
the same way in both places and adding one needs no edit here. Measured after
generating: 26 raw-slug headings, then 0, across 34 sections.

The eight curated headings stay. "API reference — one page per capability, one
per operation" says more than "openapi" does, which is the whole reason to write
one by hand.

The menu is two groups rather than eight items. Ours open directly — Hanzo App
in the browser, Hanzo CLI as a command, because it runs in the reader's terminal
against their repo and takes a task rather than a URL. Everything under "Use
with any agent" is the same page handed to somebody else's: the prompt, the
markdown twin, ChatGPT, Claude. Skills sits below both, since it is the door to
every capability rather than to this page.
2026-08-23 12:36:22 -07:00
zeekay b892ba9932 Every markdown twin an agent reads was headed "# services:"
getSection was a hand-kept table and it had drifted both ways: four names in it
(commerce, llm, mcp, services) match no directory on disk, and thirteen that do
exist — cli, start, apps, concepts, migrate, mcp-tools among them — matched
nothing and fell to a default of "services", which is not a directory either.

It has three callers. Two are dead: both read it as var(--<section>-color, ...)
and no --*-color token is defined anywhere in the app, so they have always
resolved to the fallback. The third is the markdown twin, where the wrong word
was doing real damage — measured on the live site, /llms.mdx/docs/cli reads
"# services: CLI" and /docs/quickstart reads "# services: Quickstart". That is
the surface every Copy Markdown, View Markdown and Open-in-ChatGPT link hands a
model.

The section is derived from the path now, so it cannot drift: adding a section
is adding a folder.

The twin loses the prefix entirely rather than gaining a better one. It was a
lookup inherited from the docs framework's own manual — framework / ui /
headless / mdx / cli, none of which we publish — and a heading that names the
page is true where one naming a section we do not have is worse than none.
Verified: "# Quickstart", "# CLI", "# SDKs", same byte size otherwise.
2026-08-23 12:05:45 -07:00
zeekay 14ff9e1c00 the nav row hides on a phone again: its tracks are classes, not an inline style 2026-08-23 11:46:36 -07:00
zeekay 6e5ffde46f the front door and the docs chrome state their tracks, on @hanzo/ui 2026-08-23 11:31:52 -07:00
zeekay 6a4bc1de49 A sidebar row is a name, so the API reference stops printing sentences
The operation page title was the summary cut to 80 characters. Measured over the
tree: 1,849 of 2,792 titled pages read as a truncated sentence, and every one of
them was in the API reference — so the whole of /docs/openapi rendered rows like
"Registers a new webhook subscription for the caller's org and answers 20…".

A row is scanned, not read. The title is derived from the address now: the method
gives the verb, the last real path segment gives the subject. Delete webhook, Get
webhook, List deliveries, Test webhook. The sentence loses nothing — it is
already the page description, directly under the name.

Derived rather than authored, so it cannot rot: 2,253 operations named, longest
33 characters, mean 13, none over 40. Re-measured after generating: 1,849 -> 0.

One judgement is a LIST rather than a rule. A trailing word can be an action on
the noun before it (/webhook/{id}/test) or a collection under it
(/webhook/{id}/deliveries), and nothing in the document separates them; the
actions we serve are named. A word missing from that list is wrong quietly —
"Create sync" instead of "Sync repo" — rather than a rule that guesses.
2026-08-23 11:26:55 -07:00
zeekay 660342d729 the docs shell names the row that absorbs the leftover, and the footer reflows on its column 2026-08-23 10:59:31 -07:00
zeekay 5db09dba92 The language strip shows extensions
Six full language names in a tab strip is six wide chips for a choice that is
secondary to the one above it. A developer already scans .ts and .py in editor
tabs, and they fit the row now that it sits at the right of the surface header.

The name is not replaced, it is joined:  still says TypeScript, because
prose says 'the Rust SDK' and never '.rs'. The strip reads . One table,
two projections, so a language cannot be called one thing in a tab and another
in a sentence.

Applied to both writers — the generated flow pages through the SDK table, and
the hand-written quickstart.
2026-08-23 10:48:43 -07:00
zeekay cc99e76a51 The coding agents were reachable and documented nowhere
hanzo code runs a coding agent in your repo and takes three — Hanzo Dev, Claude
Code, Codex — each named several ways (positional, flag, --backend, and the
hanzo dev shorthand). None of it appeared in the docs: grepping the whole tree
for 'hanzo code claude' or '--backend' returned nothing.

The generated CLI reference could not carry it, and that is not a gap in the
generator. It is built from operations that call api.hanzo.ai, so /docs/cli/code
correctly documents the code INTELLIGENCE API — context, file, index — which
shares the word and is a different thing. The page says so, and says how to tell
them apart: a first argument that names a backend selects one, anything else is
a task.

Written from src/commands/code/backend.rs, where every accepted spelling lives
in one table — dev, claude|claude-code|cc, codex — so the page and the binary
cannot disagree about what is accepted.

Also moves  out of 'Build with' into Guides. The Guides nav link opens
/docs/start, so the word led to a page that was not in the section it named,
while Guides held migrate and integrations without it. Build with is now exactly
the surfaces you call.
2026-08-22 21:07:55 -07:00
zeekay 48ef3e0ea7 Quickstart steps say what you do
"Your first call", "Something that costs money" and "Run the whole cloud
yourself" named the intent and not the act, so the table of contents read as a
description of the page rather than the sequence a reader is performing. They are
"List the models", "Send a completion" and "Run the cloud locally" — each the
command directly under it.

The cost framing the old title carried moves into the body, next to the command
that spends, which is where someone about to run it is looking.

Verified against the CLI's own command table rather than assumed: `models list`,
`chat completions`, `billing balance` and `usage summary` all exist. `auth
login`, `version` and `up` are absent from it by construction — that table is
derived from API operations and those three are local commands.
2026-08-22 21:03:49 -07:00
zeekay 993ed6da44 "Search or ask AI" now has a door for the asking half
The field said search OR ask; only searching worked. Full-text matches TERMS, and
a question is mostly terms that appear in no page — measured against production:
"api key", "quickstart" and "kms" all return results, "how do I get an api key"
returns none. The reader most in need of an answer got the emptiest screen, under
a label that had promised otherwise.

Ask AI was removed from this app deliberately — its floating button landed in the
same corner as the Hanzo Edit launcher and the two overlapped — and the label was
never taken down with it.

This is not that button coming back, and not a chat surface bolted into a search
dialog. It is the door this product already gives everywhere else: hand the
question to the reader's own agent, carrying the setup that points it at Hanzo's
models and the skills at hanzoskills.com. Same prompt as the Use Agent control,
imported rather than restated, so there is one thing to keep true.

It sits LAST in the list, because a question is the fallback when the terms did
not land rather than a competitor to a page that did — and it appears from three
characters, so it is there before the reader has finished discovering that
nothing matched.
2026-08-22 18:54:07 -07:00
zeekay 2d1fa7338b A nested tab row is a refinement, not a second widget
Picking SDK opened a bordered, filled box inside the bordered, filled box that
offered it — a frame in a frame, with the six languages reading as a separate
control bolted underneath the surface row.

One level down is the SAME choice refined: which language, having picked the
SDK. So it shares the parent's frame — no border, no fill, no margin — and its
triggers sit at the RIGHT of the row, smaller, where a secondary choice belongs.

Nesting is read from the context the parent already provides, so nothing has to
be passed and no caller changes. No negative offset to tuck the row into the
header: that would assume a header height, and the header grows whenever a
label wraps.
2026-08-22 18:23:25 -07:00
zeekay fd9b006d31 One sparkle, and the quickstart stops warning about a command nobody typed
THE ICON. `Sparkles` is one four-pointed star with two small ones trailing it
diagonally, and it stood for intelligence in five places — the Use Agent control,
the landing card, the model catalogue's badge for our own families, the
capability icon map and the pricing page. `Sparkle` is the same star with the two
trailing ones removed: one mark, four legs, which is what the brand's own
geometry is made of.

THE WARNING. The quickstart interrupted "log in" to explain that `hanzo login` is
not a command and would instead open an agent and ask it about logging in. That
is a note about the CLI's argument parsing placed where a reader is trying to log
in — it teaches the wrong string by naming it, and the section already shows the
right one on the line above. `hanzo auth login` and nothing else.

The one remaining warn callout on these pages stays: it says a write REPLACES
rather than merges, which is a fact about what the call does to your data.
2026-08-22 18:19:34 -07:00
zeekay c93407fb90 the png that outvoted the favicon is gone, the code theme has one home, and MCP leads
icon.svg was already fixed to the brand mark, but icon.png stayed: Next
emits a link for every icon convention it finds, so an opaque 256px tile
sat beside the SVG and browsers that prefer PNG still drew the tile.

lib/shiki.ts said vesper while source.config.ts restated catppuccin twice
next to it, so the file named for the theme lost. Catppuccin italicises 24
scopes to vesper's 7, which is why the quickstart rendered 'npm i -g hanzo'
with the command word slanted.
2026-08-22 18:16:46 -07:00
zeekay fbcd60038d The front page teaches the same install the quickstart does
It showed two blocks: a terminal with curl hanzo.sh, and pip install hanzoai.
So the front door taught one install line and /docs/quickstart taught four, and
the language a reader saw first was Python because it was the only one there.

Both now lead with npm — the one most readers already have — and carry the same
tabs in the same order: npm, Script, Homebrew, pip to install; CLI, TypeScript,
Python, Go, HTTP to use. Measured before writing them: npm 'hanzo' 8.5.90 ships
bin:hanzo, hanzo.sh serves a 12,752-byte installer, and PyPI 'hanzo' 0.4.4 does
provide the command while 'hanzoai' 8.5.89 is the API client — so the install
tab and the client tab name different packages on purpose.

Use Agent renders on every page now. It returned null off /docs, which took it
off the front page — the page a reader is most likely to hand to an agent. Only
the two Markdown items are gated, since only a doc page has a markdown twin.

Mission Control is not a thing and Studio was an empty folder that would render
a heading over nothing; both leave the rail, and Interfaces is Apps.
2026-08-22 18:08:32 -07:00
zeekay cf8cb09cb4 A capability is four surfaces, and none of them linked to the others
`ai` is an API reference, a `hanzo ai` command tree, an MCP tool and an SDK
method. Each is generated by a different script into a different section, and
measured before this: all three linked to the API reference and NONE of them
linked to each other. A reader on the CLI page had no route to the tool that does
the same thing; a reader who arrived from a model had no route to the reference.

One row on all three, naming the four with the current one in bold, so the set is
the same everywhere and only your position in it changes. 273 pages, 126
capabilities.

IT IS A POST-PASS, and that is the whole design. Each generator names its pages
with its OWN spelling of the capability and the spellings differ in both
directions — the MCP page for the `affiliates` tool is filed under the product
`affiliate`, and the CLI tree for `prompt` is at `prompts`. So no generator can
be handed its peers' sets: whichever runs first reads a directory that is not
written yet, and DERIVING a peer's spelling is a second copy of a decision that
generator owns. Both failures are measured rather than argued — 48 broken links
deriving MCP names from the vendored tool list, then 26 more in the other
direction once the order was flipped to fix it.

Reading the finished directories has neither problem: the pages that exist are
the addresses that exist. A capability whose spellings genuinely differ loses the
link rather than getting a guessed one — a missing link costs a reader a search,
a wrong one costs a 404 and the credibility of the rest of the row. Now 0 broken
of 273, and the pass is idempotent: a second run rewrites nothing.
2026-08-22 18:05:01 -07:00
zeekay c8d7a31fbb A public docs site stops advertising repos nobody can open
Of 113 projects listed, 43 linked to GitHub URLs that 404 for any reader: 39
private, 4 deleted. kms, rust-sdk, node, mq and treasury were among them — a
reader following the Open source rail to an SDK got a 404 from GitHub.

Visibility is the rule, so there is no dead-repo list to keep. operator and paas
are private and drop out on their own; both were superseded (paas says so in its
own description, '[SUPERSEDED by hanzoai/platform] ... kept for history'), and
hanzoai/id is here because it is public as of today.

Manifest 113 -> 70. Removed 25 flat pages for private repos and 5 directories
for repos that no longer exist under the org (arc, frappe, runtime, s3-cli,
video). Nothing left in the tree resolves to a non-public repo.

The stale manifest was the March snapshot, tracked in git while its output
directory is ignored, so it survived every checkout that rewrote the tree around
it. The sync already filters private repos when it builds the list; what was
missing was ever regenerating what it had written.
2026-08-22 18:02:00 -07:00
zeekay 8f5909cb19 A slow page is not rebuilt twice more for being slow
The deploy builds per sha and never cancels, deliberately: sharing a concurrency
group put every run in `waiting` until the next push swept it, and no image was
pushed at all. The cost of that correctness is that a burst of pushes runs
several full 6,099-page builds at once on one box.

Under that contention pages cross the 60-second default and are RETRIED —
measured on a five-build burst, /docs/console, /docs/credits and /docs/enso each
failed "attempt 1 of 3" while the run was two-thirds through its pages. A retry
does not make a contended page faster. It makes the same page cost three times,
on the box that was already the constraint.

Three minutes is well above what any page here takes with the box to itself, so
the normal path is unchanged; only the pathological one stops multiplying.
2026-08-22 17:55:24 -07:00