23 Commits
Author SHA1 Message Date
zeekayandHanzo Dev db13b2a84a chore: symlink AGENTS.md + GEMINI.md + CLAUDE.md -> LLM.md
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-02 02:29:26 -07:00
zeekay 625fa41deb Give every package one version 2026-08-29 18:44:18 -07:00
zeekay 68838604ef Name Zen where the site already serves Zen
The two variable faces under public/fonts are Zen and Zen Mono, but three
places still said Geist. The OG card's source SVG asked for a family the
site does not serve, so rsvg resolved it out of whatever the build host had
installed. The repo notes claimed Geist was served from /fonts/ and pointed
at an OFL.txt that is not there — the notice sits at LICENSE-Zen.txt. The
stylesheet comment narrated a dead CDN path instead of the rule that keeps
the faces on this origin.
2026-08-28 12:01:29 -07:00
zeekay b5be764822 fix(type): the bytes are Zen, not a Geist file wearing its name
An earlier pass renamed the families in index.css to Zen and left the files
alone, so @font-face declared 'Zen' over Geist-Variable.woff2 — whose name table
reads Geist. That is the worst shape this can take: every grep says Zen, the
page renders Geist, and nothing downstream can tell. It is the third instance
found in this estate, after @hanzo/design shipping Geist as Zen-Variable.woff2
and zoo's foundation declaring Zen over inter-var-latin.woff2.

The two faces are @hanzo/font 1.9.2's, checked by name table rather than by
filename: family Zen and Zen Mono. The licence beside them was Geist's alone —
"Copyright (c) 2023 Vercel" and no Hanzo line — which is the wrong notice for
these bytes; @hanzo/font's LICENSE.txt carries both holders, as OFL §2 asks of
the copy that ships.
2026-08-28 11:36:48 -07:00
zeekay 87e45c8878 fix(type): the binding is named for the face it holds
A docs-site template repeated across these repos named three different faces in
one place: a binding called `inter` or `geist`, constructed from next/font/google
so it actually held Inter, publishing a custom property called
`--font-geist-sans` that @hanzo/font does not set. Whichever of the three a
reader trusted, two of them were wrong.

They take Zen and ZenMono from @hanzo/font — bindings named `sans` and `mono`
for the role they fill, setting --font-zen-sans and --font-zen-mono, which is
what the package publishes.
2026-08-28 08:34:00 -07:00
zeekayandHanzo Dev 5148061f22 tell researchers the address to use before they resort to guessing one
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 19:11:02 -07:00
zeekay c48110f2a3 retire the GitHub Actions workflows
Actions are disabled on this repository, so these files cannot run: they are
build definitions no forge executes. Builds go through the native path --
POST /v1/runner, which schedules a BuildKit job -- and CI lives under
.hanzo/workflows.
2026-08-07 20:48:34 -07:00
zeekay f42fa0f09d retire the GitHub Actions workflows
Actions are disabled on this repository, so these files have not run in a long
time and cannot run: they are build definitions no forge executes. Builds go
through the native path -- POST /v1/runner, which schedules a BuildKit job --
and CI lives under .hanzo/workflows. A dead workflow that still looks like a
pipeline is worse than none, because it reads as coverage nobody has.
2026-08-07 09:25:10 -07:00
zeekayandHanzo Dev 214fe94c40 on: push fires here only sometimes — say that, with the runs
Yesterday's note said pushes never trigger a run; I replaced it with "a push to
main publishes" on the strength of run 30823263229. Both are wrong. Two pushes
today, same repo, same workflow, same pusher as the one that worked, started no
run at all; workflow_dispatch started one both times. The table is in LLM.md.

What matters is the rule that survives either explanation: a merge is not
evidence of a deploy on this host. Check for a run, dispatch if there is none,
and let the job's live re-fetch be the thing that says it landed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:33:53 -07:00
zeekayandHanzo Dev 4a6aaa2982 no-transform: Cloudflare was injecting its beacon into the page
Routing `/` through a Worker changed one thing nobody asked for: this zone has
Web Analytics auto-injection on, and Cloudflare appends a
static.cloudflareinsights.com <script> to HTML a Worker returns. It leaves plain
assets alone — /page.html comes back byte-identical to the build — so the page
carried no beacon while it was a static file and picked one up the moment `/`
became a Worker response. hanzo.ai and hanzo.bot carry none. This host does not
ship third-party script it did not write; `cache-control: no-transform` is the
standard way to say so.

The deploy gate stops comparing `/` byte-for-byte against the built document,
because an edge injection the Worker requests and does not control must not be
able to fail a deploy. It compares the document at /page.html exactly, still
compares the installer at `/` exactly, and asserts about `/` what actually
matters: a browser gets 15 bytes of <!DOCTYPE html> and a page that names this
build's content-hashed stylesheet. That is the check that catches staleness,
which is the failure this host has actually had.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:28:46 -07:00
zeekayandHanzo Dev 9603d6cbdc delete the image path that cannot serve this host, and say why
Dockerfile and .hanzo/workflows/deploy.yml drafted a move to
ghcr.io/hanzoai/static behind hanzoai/ingress. They had never produced an image
— the forge job names a runner pool this org does not register, so it queued to
the 24h timeout in silence — and with the polyglot gone they cannot produce a
working one: hanzoai/static answers with http.ServeContent and has no way to
choose a representation from Accept, so `/` would be the page or the installer,
never both. Its asserts (`head -1 == #!/bin/sh` on dist/index.html) now fail by
construction. A Dockerfile that can only build a broken image is a trap; the
requirement it is blocked on is written down in LLM.md instead.

README: the deploy command named hanzo-apps/hanzo.sh, which is the wrong org —
the repo is hanzoai/hanzo.sh — and claimed pushes to this repo create no runs,
which run 30823263229 (push of 307e4fb, 2026-08-03) disproves. Both fixed, and
the local recipe now runs the real Worker over the real assets instead of
python3 -m http.server, which cannot answer `/` at all under the new layout.

LLM.md carries the rest: the routing rule, why the polyglot could not be
repaired, where the mark and the typeface come from, and the one defect this
repo cannot fix from inside — Cloudflare Managed Content synthesises a
/robots.txt for this zone that Disallow: / for ClaudeBot, GPTBot,
Google-Extended and CCBot, on the one host that publishes /llms.txt for exactly
those readers. Shipping public/robots.txt does not win; the zone toggle does.
hanzo.ai and hanzo.app do not have that block, hanzo.bot does.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:16:57 -07:00
zeekayandHanzo Dev efd925d977 one URL, two answers: the page and the installer stop being the same file
dist/index.html was a polyglot — `#!/bin/sh`, a `<<\EOF` heredoc swallowing the
whole document, the installer after the closing EOF. Both halves ran. The
browser half was never honest, and could not be made honest.

Anything before <!DOCTYPE html> that is not whitespace or a comment puts the
document in quirks mode. Chromium, at 390 and 1280: compatMode BackCompat;
document.head.children.length === 1, with all fifteen head elements — title,
icons, viewport, description, every og: and twitter: tag — parsed into <body>
where they do nothing; and "#!/bin/sh <<\EOF" as the first text node of the
page, first string in body.innerText, pushing <main> down 24px. Proven by
re-serving the identical live bytes with the 17-byte prefix stripped: BackCompat
-> CSS1Compat, head.children 1 -> 15, at both widths.

No arrangement of those bytes avoids it. Only whitespace and comments may
precede a DOCTYPE; an HTML comment opens with `<!`, which a shell reads as a
redirect from a file named `!--`, so a POSIX script cannot start there at all.
The trick was unfixable, not unlucky.

So the two resources become two files and `/` chooses between them by Accept,
which is what HTTP has for this. worker.js is the whole rule: text/html gets
dist/page.html, anything else — curl, wget, the /cli /mcp /dev shims that
re-exec `curl -fsSL https://hanzo.sh` — gets dist/install.sh. `/` carries
vary: accept and cache-control: no-store, because Cloudflare honours Vary only
on Accept-Encoding and a shared cache holding one representation for the other
kind of client breaks either the page or the install command.

scripts/postbuild.js replaces the polyglot rewrite: it moves the document off
index.html (Static Assets serve an exact match BEFORE the Worker runs, so a file
at that name would hand curl the HTML) and writes /install from install.sh, so
the alias is one file in git instead of 12,746 duplicated bytes.

wrangler.toml: routes moves ABOVE [assets]. TOML puts every key after a table
header inside that table, so it was parsing as assets.routes and wrangler
dropped it with a warning — the custom domain survived only because it was
attached out of band. html_handling and not_found_handling are "none": exact
match or the Worker, nothing silently rewritten, a mistyped path still 404s.

The deploy gate now checks both representations against the live URL, plus a
200 for every path the head declares — the check that would have caught
og-image.svg. Verified against the real runtime before pushing: wrangler dev
--local, curl gets install.sh byte-for-byte, a browser gets page.html with
<!DOCTYPE html> at byte 0, `sh -n -c "$(curl -fsSL /)"` parses under dash and
bash, and /index.html /nonexistent-xyz /some/deep/path still 404.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:16:34 -07:00
zeekayandHanzo Dev 09b1fada81 the estate icon set, an og image that exists, and the brand typeface
Three things the page declared and did not have.

Icons. /favicon.ico and /apple-touch-icon.png were 404 — hanzo.ai serves both —
and /favicon.png was 8-bit RGB with no alpha, so the rounded corners of the tile
were filled pure white and read as four white nubs on a dark tab strip. The set
is now the estate's, byte-identical to the files hanzo.ai serves and to
hanzoai/brand assets/logo/favicon.svg: .ico (16/32/48), the theme-aware .svg,
16x16 and 32x32 PNGs, and a 180x180 apple-touch-icon. Nothing redrawn.
public/hanzo-logo.svg goes with them: a second copy of the mark published at a
public URL and referenced by nothing.

The card. og:image pointed at /og-image.svg, which is in no commit of this repo
— every social card resolved to a 404, and it was relative, which not every
scraper follows. It is now an absolute /og-image.png, 1200x630, built by
rsvg-convert from scripts/og-image.svg: the canonical five-path mark and copy
that is already on the page, nothing invented, plus the width/height/alt that
let a scraper lay it out without fetching.

The typeface. src/index.css @imported Geist from
cdn.jsdelivr.net/npm/[email protected]/dist/fonts/{geist-sans,geist-mono}/style.css,
a path that package has not had since 1.4. Both returned 404, Chromium reported
ERR_BLOCKED_BY_ORB, document.fonts.size was 0 — no @font-face rule ever
existed, and every visitor read this page in their system fallback. Measured
with the local Geist hidden from fontconfig: rendered width of the same string
was 1191px against a 1193px system fallback (identical), and is 1328px now.
Nothing surfaced it because a cross-origin fetch that fails is not a console
error. The two variable files ship from /fonts (OFL, public/fonts/OFL.txt).

Also: the footer said (c) 2016 and the installer served from the same URL said
2024-2026 hanzo ai inc. Two founding years and two spellings of the company on
one host. Both now say what hanzo.ai says — the current year, "Hanzo AI, Inc." —
and the installer header carries no year it cannot keep current. The two depth
slivers of the inline mark are 0.867 opacity, which is the canonical #DDDDDD
over black; they were 0.7.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:16:04 -07:00
zeekayandHanzo Dev 312b0fa0bb drop the scaffold vendor's build plugin, still in the build config
vite.config.ts imported componentTagger from the scaffold vendor's plugin and package.json
carried the dependency. It only ran in dev mode, so it never reached a
production byte — but it is the vendor's fingerprint in the build of a Hanzo
host, and it pulls 837 lines of lockfile for a plugin nobody here uses. The
the scaffold generator's CDN script tag it came with is already gone from index.html.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:15:31 -07:00
zeekayandHanzo Dev 307e4fb626 serve /install as well as /install.sh — the path the deployed caller requests
hanzo.ai/install.sh execs `curl -fsSL https://hanzo.sh/install | sh`. This site
serves install.sh and not install, so that fetch 404s; -f writes nothing and
exits 22; and sh reading an empty script exits 0. The documented one-liner —
curl -fsSL hanzo.ai/install.sh | sh — reports success and installs nothing.

The caller is already corrected in hanzo.ai, but that deploy has not shipped, so
the break is live right now. Serving the path it asks for repairs it from this
side immediately, with no dependency on the other pipeline.

Extensionless is this site's existing convention rather than a new one: cli, dev,
agents, bot and full are all served that way and hanzo.sh/cli answers 200 today.

The bytes are install.sh's, verified identical to what production serves, and the
shebang is #!/bin/sh — which matters, because the caller pipes this into sh and
not bash. The two files must stay identical: install.sh is the name the docs and
release notes use, install is the name the deployed caller asks for.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 07:33:23 -07:00
zeekayandHanzo Dev 82559b21cb install dev, and delete the template page rendering under the real one
`hanzo code` is the CLI's headline command and it runs `dev` by default. A
public `curl hanzo.sh | sh` did not install `dev`, so the first thing a new
reader ran answered:

  Error: the `dev` coding agent is not installed (not on PATH)

The two reasons the installer named for skipping it are both gone —
hanzoai/dev is public and publishes `dev-<os>-<arch>.tar.gz.sha256` — so it is
one row in TOOLS, exactly as LLM.md said it would be. With no second name: the
alias defaults to `hanzo-node`, which the CLI owns, and handing that name to a
different program is how the delegate silently changes identity. Verified end
to end in a clean HOME under both dash and bash: hanzo 1.9.18, hanzo-mcp
1.1.15, dev 0.6.91, and `hanzo code` now starts Hanzo Dev instead of erroring.

The page rendered four components, not one. Hero is the whole page and carries
its own header and footer, so the leftover scaffold-template Navbar, Features
and Footer rendered a SECOND fixed nav, four invented feature cards, sixteen
`href="#"` links that go nowhere, "Simplifying application development and
deployment with innovative container solutions" for a company that does not
sell containers, and a `© 2024` ten pixels under the real `© 2016–2026`. All
five leftover components deleted. Rendered proof: 1 header, 1 footer, 0 dead
links, 11 links and every one resolves.

Two JSX text nodes shipped their escapes as text — the page literally read
`source →` and `— the same build`. Fixed; 0 literal escapes render now.

llms.txt: `hanzo usage` is a group, not a command (it prints subcommands and
stops) -> `hanzo usage summary`. `hanzo engine serve enso` was published as if
it worked; it needs the Hanzo engine binary this installer does not ship, and
says so — stated as a gap rather than left to be discovered. Added the model
ids that exist, the public catalog URL, and why `hanzo login` is not a command.

The installer's version line printed the last field of `--version`, which is a
build date for `dev` (`✓ dev 2026-04-01`); it now takes the first token that
looks like a version. "installable today" is read off TOOLS instead of being a
second hardcoded list.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 13:39:23 -07:00
zeekayandHanzo Dev 76c2073781 build: migrate tsconfig to TypeScript 7 (native compiler)
TypeScript 7 is the native Go compiler and removes `baseUrl` and
`moduleResolution: node|node10`. Both appear here, so `tsc` from TS7
refuses the config outright (TS5102 / TS5108) and cannot typecheck.

`paths` targets resolve relative to `baseUrl` when it is set and relative
to the tsconfig file otherwise. Every `baseUrl` folded here already
pointed at the config's own directory, so dropping it moves nothing and
the targets are left byte-identical. Where a baseUrl pointed elsewhere,
each affected target was rewritten as join(baseUrl, target).

`moduleResolution` was chosen from the declared `module`: commonjs ->
node16, esnext/preserve -> bundler. Configs whose `module` is unset or
exotic were left alone rather than guessed at.

The result is accepted by BOTH toolchains, so nothing has to upgrade
TypeScript in lockstep. Verified on hanzo/chat packages/api: tsc 5.9
779 -> 778 errors (no regression), and tsc 7.0.2 now runs the project
in 2s where it previously refused the config.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 14:18:19 -07:00
zeekayandHanzo Dev 39c4967259 serve hanzo.sh from hanzoai/static, not Pages or a CF Worker
.github/workflows now holds sync.yml alone, carrying refs to git.hanzo.ai; every
build runs natively there from .hanzo/workflows.

.hanzo/workflows/deploy.yml publishes ghcr.io/hanzoai/hanzo-sh:<sha> and does not
deploy — the operator CR in hanzoai/universe names which tag is live.

The GitHub Pages workflow it replaces never served this host: Pages reports
status null (no build ever succeeded) and hanzo.sh resolves to Cloudflare, where
an assets Worker answers. Its two jobs asked for hanzo-build-linux-amd64 and
hanzo-deploy-linux-amd64, retired labels nothing answers. public/CNAME goes with
it: that file is a Pages control file and means nothing to hanzoai/static.

The Dockerfile ends FROM ghcr.io/hanzoai/static:v0.5.1 with dist at /public, and
asserts the polyglot before the image exists: dist/index.html must keep its
`#!/bin/sh` first line, its `<!DOCTYPE html>`, and parse under bash -n, because
`curl hanzo.sh | bash` runs the same bytes the browser renders. Measured: static
returns the live 25426-byte polyglot byte-identical as text/html, and the served
response still runs the installer under bash.

wrangler.toml stays until the image is promoted; removing it first would strand
the live host.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 08:45:51 -07:00
zeekayandz 52207b4cfa install: warn when a stale binary shadows what we just installed
A binary earlier on PATH permanently masks the one the installer writes, and
the failure is SILENT in the worst way: the install reports success, the user
runs the old copy forever, and re-running the installer never fixes it because
it writes to a DIFFERENT directory.

Found live: ~/.local/bin/hanzo held a retired Go build (it reports version
'dev') while the canonical Rust CLI installs to ~/.cargo/bin. uv also installs
tools into ~/.local/bin, so a Python `hanzo` shadows the same way. Someone in
that state debugs a CLI they are not running.

check_shadowing compares `command -v <bin>` against where we actually put it,
for hanzo/mcp/dev/hanzod, and prints the exact mv to fix it. It does NOT delete
anything — the user's files are theirs — but staying quiet is what makes this
cost hours.

Verified all three cases with a harness against the real function:
  stale first on PATH  -> warns, naming both paths
  canonical first      -> silent (no false positive)
  canonical not present-> silent (nothing to shadow)
(My first harness run passed for the wrong reason — I had created `cargo/bin`
instead of `.cargo/bin`, so the check correctly found nothing. Fixed the
harness, then it caught the case.)
2026-07-26 00:47:52 -07:00
zeekay 8d8dc1171f chore: add MIT LICENSE 2026-02-14 05:26:47 -08:00
zeekay e178709784 feat: add llms.txt for AI agent discovery 2026-01-28 11:22:20 -08:00
zeekay ec29197155 Update README 2025-02-22 00:25:24 -06:00
zeekay a2a7461f9a Update README 2025-02-22 00:22:52 -06:00