The first state's first call is begun as soon as the state is ranked, against its revision. The
search settling the task, or a kept step moving the state, cancels it; a cancelled call closes its
stream and leaves an estimated line in the spend file. Past its cap the search goes on while that
call has not answered, to 8 s, and a visible test too slow for the cap is narrowed to the failing
tests it names instead of the search being skipped. The tests around the failing ones run beside
the visible test, and what failed among them before any edit is read only when they fail after
it. A test run in the tree leaves no file git neither tracks nor ignores, and Hypothesis keeps its
examples in the run's own temporary directory.
Model-free on 60 dev-split tasks on evo, A/B interleaved: search-settled tasks 1.74x faster (95%
1.64-1.85, 13 of 13), solves 13 to 19: five exact pyrsistent fixes no longer fail on the
.hypothesis files the runs wrote, and a lark task with an 18 s test file is searched.
hanzoai/codex e7a4d438: the update check reads hanzoai/dev's latest release and
offers the installer that put Dev there (@hanzo/dev by npm, bun or pnpm, the
hanzoai/tap/hanzo-dev formula, hanzo.sh otherwise), and the empty-state
animation draws the Hanzo mark. 6fd3f345: the fork's doctor, daemon and composer
snapshots say Dev. install-context now reads its npm marker from `hanzo`, which
is the lock's one new edge.
hanzoai/codex names nothing outside its tree now (d1d710452): Dev's names,
defaults, model catalog, `/model <name>` match, controller interface and
version live in its `hanzo` crate, and Dev's crates depend on that, never
the reverse. hanzo-loop moves there whole; hanzo-tui keeps only the Hanzo
account; hanzo-config reads API_BASE, DEFAULT_MODEL and is_hanzo from it.
hanzo-dev's main registers what Dev adds before anything starts:
`codex_app_server::register(hanzo_kai::install)` puts Kai on every thread,
and `codex_tui::register(Account { login, logout, show })` puts the Hanzo
CLI's sign-in behind /login, /logout, /auth and /status. launch.rs holds
that /login is offered, which it is only when an account is registered.
make test-upstream runs locked against the fork's own lock.
Nothing in release.yml, hanzo.yml, the Makefile or scripts/ names any of
them: the zsh release script and its dotslash config (upstream dropped the
zsh fork), verify_cargo_workspace_manifests.py and
verify_tui_core_boundary.py (checks of a codex-rs this repository does not
hold), the Linux, macOS and Windows code-signing actions and the macOS
notarizing scripts (upstream's identities), and upstream's issue templates,
which asked about the Codex app and CLI.
`copy` reached for std::os::unix::fs::symlink unguarded, so the windows-amd64 build of v0.7.0 failed at
hanzo-kai while the four other targets built. Each platform's symlink call sits under its cfg; the
other two unix-only sites in the Hanzo crates were already gated at the function and module.
hanzo-config, hanzo-kai, hanzo-loop and hanzo-tui keep 0.0.0, and each
says why beside it: upstream crates depend on them, so a version that
moved would recompile everything above them. None is a crate anyone
installs from a registry, so `publish = false`.
Nothing here runs Bazel: make, hanzo.yml and release.yml build with cargo.
The root carried upstream's own Bazel setup anyway: MODULE.bazel (which
already named a patch that was not here) and its lock, BUILD.bazel,
.bazelrc, .bazelversion, .bazelignore, defs.bzl, rbe.bzl, bazel/, the test
launcher templates, third_party's BUILD files and V8 wiring (scripts/v8.sh
reads the fork's), the buildifier pin and format.py's Starlark group, the
Bazel CI actions and scripts under .github (setup-rusty-v8-musl read its V8
version through one of them), and argument-comment-lint, whose runs go
through a Bazel aspect over codex-rs, which is the fork's. hanzo-dev's
cargo-shear note named Bazel-only benches that do not exist.
The version, by hand: the root Cargo.toml's [workspace.package] (which
hanzo-dev alone takes), npm/package.json and its platform packages, and
the lock. The changelog says what 0.7.0 carries: openai/codex at
3ee47c105 through hanzoai/codex, the delta as commits there, Kai's kept
calls and repaired answers, and versions assigned by hand.
upstream/codex is now hanzoai/codex, whose main is openai/codex with
Hanzo's commits on it, and dev builds that checkout as committed: no
`make prepare`, nothing rewritten, `git -C upstream/codex status` empty
after a build and a test run. A bump is `git merge upstream/main` on the
fork's main, with conflicts in git, then the gitlink here.
openai/codex moves from 11da6b9e to 3ee47c105 (288 commits). Of the 106
anchored edits one had lost its ground, the --sandbox-state-json help that
upstream reworded, and was re-anchored before the delta became commits on
the fork: hanzo (Dev's crates in its workspace), tui, cli, config, core,
words, brand (the name rule's 227 literals) and version (the version
rule's 126 reads). The edit that set upstream's workspace version to
0.7.0-dev.1 is dropped: no upstream code reads its own version any more.
None of the anchors touched the zsh fork, shell-escalation or gateway_auth
code upstream removed or renamed, and the Hanzo crates built against the
moved APIs unchanged.
The version: hanzo-version moves into the fork, hanzo-dev hands it its own
CARGO_PKG_VERSION at startup, and DEV_VERSION is gone. The root
[workspace.package] version is the release's, and release.yml holds the
tag to it as well as to npm/package.json. Dev's libraries stay 0.0.0,
since upstream crates depend on them, so a release still recompiles
hanzo-dev alone. cli.rs fails on any upstream read of its own version.
Gone: patches/ (upstream.json, brand.json, and the Bazel patches only the
root MODULE.bazel names, which no build here runs), crates/hanzo-upstream
and crates/hanzo-version. The V8 fetch for `make test-upstream` is
scripts/v8.sh; `make bump` prints the merge steps; `make reset` goes. The
root Cargo.toml mirrors the fork's workspace dependencies: shell-escalation
and mxc-sdk out, the split mxc crates, gix 0.89, rmcp 3.5.1 and rustls
0.23.46 in.
Nothing built from it: its crates carry their own codex-core, so linking
them beside ours would put two cores in one binary. Its clone lives at
~/work/just-every/code; what was ported from it is recorded in migration/.
The submodule sat at 41f66ca5d, their main.
A release runs from a tag vX.Y.Z and ships that version: the tag must equal npm/package.json's version
(checked twice, in the version job and before npm publish), a version already published is refused, and
nothing is committed, tagged or bumped from CI. Until now every push to main took the next patch above
whatever was published and landed a chore(release) commit; the owner's rule is manual semver everywhere.
The Homebrew formula is generated at publish and pushed to the tap alone, so its in-tree copy goes.
npm/package.json: 0.6.185, the first version assigned so.
fix.turn@1 answering escalate, two failed attempts, or recover finding
nothing changed hands the next attempt to generate.escalate (openai/gpt-6-sol
at Hanzo's API, never an Anthropic model); the ledger records the decision
selected and applied and the model that wrote each edit. A run that passes
the failing command is not done until the pytest directory around it shows no
new failure, capped at 20 s. Every model call is kept by the hash of its
request and a repeat is answered from there, marked cached.
A candidate that leaves the code unreadable or adds tests that cannot run is no
step. kill -9 -PGID without -- killed nothing, so a looping candidate kept its
pipe and hung the run: every test the flow starts is now killed with its
children, and a search stops its running tests at the cap. A test the runner
says takes longer than the cap holds is not searched. A revert returns to the
base revision, one search a state. The model is asked for one JSON object, with
a timeout a call, and is told which edits were already tried.
dev flow fix reads the failing output against the tree (locate: the gateway's
learned ranker, features held to its own by a test), puts the functions tgrep
finds spelling the failure's messages into the ranking, and lets Kai choose the
site, the action and the effort in one request, or decides alone when the
ranking is sure. The action search tries generic token transforms at the best
sites against the visible test, eight at once; a candidate that leaves fewer
tests failing is kept as a step. Else a model writes one edit (applied by text,
never a test file), the test runs, Kai reads a failed run in one request, and
recover fingerprints the failure and never replays a strategy on it.
Every action is a ledger line with a stable id, its revision, preconditions,
authorization, source and the states it passed through; credentials are
redacted. Kai's transport is a trait, health is per program.
Cargo.lock is not committed: hanzo-trigram is read from hanzoai/mcp commit
e9e6096, which is not pushed yet.
On api.hanzo.ai, automatic approval review and memory extraction and
consolidation ask for enso-auto, not OpenAI's ids. Off OpenAI, the reviewer
counts a model as served only when the catalog lists it: the bundled OpenAI
entries ride along unlisted for lookups, so codex-auto-review looked served on
every provider and a LAN engine was reviewed by a model it does not have. Now
it falls back to the model the run is on.
Co-authored-by: Hanzo Dev <[email protected]>
Move both submodules to current main and bring the owned delta with them.
- patches: the session title is one literal in codex_title now, so it is
anchored there and the status-card edit, which that title replaced, goes;
get_tooltip takes the keymap; the cyber-access flag names a provider in
its help; mcp_login joins the modules the library publishes.
- Cargo.toml: workspace dependencies, profiles and patches track upstream's,
as they did at the last pin, and Cargo.lock follows.
- tests: the pty spawn takes ChildFds; the feature round trip asks for an
experimental feature instead of naming one upstream has since deprecated.
Claude-Session: https://claude.ai/code/session_01SVaQNFaAjhDxthpxasGah5
The Hanzo provider asks `dev token` for its credential every ten minutes
instead of reading HANZO_USER_KEY once at startup, so a session outlives the
hour a sign-in token lives. A profile still on the old startup-only provider is
moved to it; one the user changed is left alone. `dev token` prefers an API
key, then a fresh token from the Hanzo CLI, to a sign-in token inherited from
whatever launched the agent.
The chat models api.hanzo.ai serves carry their real window and inputs, so
enso-auto and the rest no longer run on fallback metadata.
A justification without sandbox_permissions runs at the default permissions
instead of failing the shell call. Featured plugins are asked for only with a
ChatGPT sign-in, which is the only caller that endpoint answers. The profile
links to the shared credential only when it exists, and a sign-in links first
so it writes the shared one.
Co-authored-by: Hanzo Dev <[email protected]>
v0.6.107 published its GitHub release and all five platform packages,
then stopped at `cd codex-cli`: the launcher moved to npm/ on
2026-09-07 and the release never followed it. Its dependencies are pnpm
catalog entries, which npm would publish as the literal `catalog:` that
no installer resolves, so each becomes the version pnpm-workspace.yaml
gives it. npm refuses a version twice, so every publish first asks
whether that version is already there.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
The workspace says 0.7.0-dev.1 while the release computes the next patch
above what is published, so the 0.6.107 build answered dev 0.7.0-dev.1
and publish refused it. The build job now replaces the workspace
version in every manifest that pins it, in the upstream edit that gives
upstream's crates the same version, and in Cargo.lock, so --locked
holds; built that way on ra, dev --version and dev doctor both say
0.6.107.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
The platform starts one runner per job when the job is queued. With
max-parallel 1, linux-amd64 built in 22 minutes and the four targets
queued behind it never got a runner, so the release sat with one
platform's assets.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
Since 2026-09-08 release.yml sat in .hanzo/workflows, which GitHub does
not read, and the forge runs nothing for this mirrored repository: no
push to main released anything, and a draft v0.6.107 holds no assets.
It moves to .github/workflows.
The runner there is a sandbox with no root, so no step installs a
package: the build and publish jobs check for what they need (cc, make
and perl included, for the vendored OpenSSL) and name what is missing, and the Windows build takes clang-cl, lld-link and
llvm-lib from a pinned LLVM 21.1.2 release instead of apt. The five
targets build one at a time, since the hosts serve other work. The
forge mirrors this repository, tags included, so the step that pushed
the release to it, which could only fail with a GitHub token, goes.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
hanzo-kai linked hanzoai/decision's control and program crates by git.
That repository is private: the public build, CI, and every cargo install
of dev failed to fetch it. Kai is now a client of POST /v1/decisions at
api.hanzo.ai, signed with the Hanzo credential Dev already sends; nothing
of hanzoai/decision is linked, .cargo/config.toml goes, and Cargo.lock
loses 89 packages and every private source.
The programs Dev asks (tools.select, router.model, reasoning.budget,
context.select, agent.command-risk, agent.progress, agent.complete, and
the fix flow's fix.pick and fix.next) ship in crates/hanzo-kai/programs
as typed questions with their thresholds, verdict rules and calibration
pins. The gate that reads the answers is Dev's own: a noul holds at its
threshold, a choice or score is accepted at its certainty, a verdict
rule joins the policy on allow < ask < deny only when enforced, and an
answer under another calibration runs in shadow.
When Kai does not answer (refused, timed out, 401, 404, 5xx, or no
state of the decision answered) the decision fails at once and Kai is
down for 60 s: no request is made in that window, the loop does exactly
what it does without Kai, one line goes to the log and no trace line is
written. Only a decision asked after the failure brings Kai back. A
decision's states are asked in parallel; tier and budget together. The
tool shortlist is decided again when the tools on offer change, since an
MCP server that finishes starting mid-turn adds its tools.
dev flow is now dev flow fix, a fixed pipeline rather than a Decision
Program run by the private executor: retrieval, Kai's pick, Zen's diff,
the tests, Kai's next. --control rule, or Kai not answering, leaves the
tests and the budget to decide, and the report says so once. An attempt
whose patch no longer reverses ends the flow instead of retrying over it.
Tests serve the Decisions API and chat completions from wiremock: Kai
on (tools shortlisted, tier and effort hinted, verdict tightened, one
trace line per decision with its distributions), Kai answering 404,
Kai unreachable, Kai answering with a page that is not the API, tools
arriving mid-turn, and an answer under another calibration.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
hanzo-loop is the contract the loop applies: a tool shortlist per step, a
command verdict joined with the policy on allow < ask < deny, and routing
hints on the turn metadata. hanzo-kai links hanzoai/decision's control and
program at ec8207d: one controller per thread answers those three through
seven anchored upstream edits, and extension contributors feed it context,
progress and completion. kai.toml turns it on, every operation in shadow by
default. Each decision is one trace line in the shape Enso's control writes.
dev flow runs a Decision Program over the repository. The shipped fix flow
retrieves candidate files, Kai picks one, Zen writes a diff, the tests run,
and Kai says done, retry or escalate within what the tests and the budget
permit. go/dev.wasm is rebuilt because Cargo.toml, Cargo.lock and the
Makefile are among its recorded inputs.
Claude-Session: https://claude.ai/code/session_018Gt5TvrE2rxXuZikWSauv3
The core has taken the event before Step decodes its answer. Step answered a
decode that failed with ErrMalformed, which says nothing changed, and with the
actions it had read so far; the session ran on ahead of its host, and the same
turn stepped again was a redelivery that asked for nothing, so its ask was
lost. Now an answer that is not read ends the session as an ended context
does: ErrHandle, then ErrHandle for every call, and the snapshot from before
the event carries it on. A Step that answers an error answers no actions. A
stand-in core that answers with a member Action lacks, and the real core with
an answer the host refuses, each fail on the old code. The memory test drops
the host refusal it looped on, since one now ends the session.
Nothing noticed if Session.Close stopped closing the instance: the closed
session test now calls the old instance and requires it to fail.
Docs: a null member that is not an Option reads as its zero value, which serde
refuses; a session that trapped answers ErrPoison after its Core closes; and
the 64 MiB cap holds about 14 MiB of conversation and 8 MiB in one event. An
ask traps at 15 MiB of results, a Snapshot at 16, a lone prompt at 9, and a
restored session meets the same wall.
Co-authored-by: Hanzo Dev <[email protected]>
CI builds on another kind of host than the one that commits go/dev.wasm, so it
cannot compare bytes, and its go step passed whatever module was committed. A
crates/core whose cancel leaves the queue behind, committed beside the old
module, went green: the Go suite passes on both, and they answer that cancel
differently.
make wasm now writes go/dev.wasm.sum: the sha256 of every tracked file the
build reads (the three crates, Cargo.toml, Cargo.lock, rust-toolchain.toml and
the Makefile) and of the module. make wasm-check hashes the tree again and
diffs; CI runs it first. On that tree it fails on crates/core/src/lib.rs, after
make wasm it passes, and a rebuilt module committed without its sum fails on
go/dev.wasm.
Co-authored-by: Hanzo Dev <[email protected]>
The remap sat in target.wasm32-wasip1.rustflags, which cargo drops whole once
RUSTFLAGS is set: RUSTFLAGS=-Dwarnings make wasm wrote a module with five
/Users/z/.cargo paths in it. It now goes in CARGO_ENCODED_RUSTFLAGS, which
outranks every other source, and that build gives 9d28969b again. The build
names --target-dir target, so a CARGO_TARGET_DIR no longer sends it elsewhere
while an older module is copied. wasm needs prepare, as build and test do, and
rust-toolchain.toml lists wasm32-wasip1, so rustup installs it.
The Makefile said the bytes are the same wherever they are built. They are the
same on one kind of host, whatever the directories. Across hosts they are not:
cargo hashes the host into every crate that has a build script or uses a proc
macro, and this Mac, Linux arm64 and Linux x86_64 build 9d28969b, 2b40ba79 and
5648efc6, code sections and all. So CI cannot compare its build with the
committed one byte for byte. It runs the Go tests on the committed module, and
again on the one it builds from the crates beside it.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/wasm built its runtime without CloseOnContextDone, so neither a context
nor the Run bound reached the core. A Step under a cancelled context answered
as if nothing had happened, and a module whose dev_abi loops held Open past its
deadline, and would hold every garbage collection with it. The pin moves to the
hanzoai/wasm that turns it on. A call its context ends closes the instance and
answers ErrHandle, which also matches context.Canceled or
context.DeadlineExceeded; the last snapshot restores the session.
enter read gone after letting go of the session, so a Close on another
goroutine raced it. -race finds it when a gone session is stepped and closed at
once. The read now happens while the call still holds the session.
alloc handed dev_alloc's i32 the whole length, so 4 GiB and 16 bytes were lent
as 16 and writing the event poisoned the session. A length past MaxInt32 is
ErrNull before the core is asked.
Decoding took null for a variant's value, and no union at all where Action,
Done or Dispatch hold one. serde refuses both, and so does Go now. Any other
missing member reads as its zero value, and the docs say so.
Close no longer calls dev_drop and dev_release into an instance it closes on
the next line. A test repeats an event the core refuses and an answer the host
refuses: releasing the loan only on success grows memory from 1.4 MB to 30 MB,
freeing the answer only on success from 1.18 MB to 1.31 MB, and each fails.
Restore's doc says the checksum is not a signature.
The suite passes under -race here, and in linux/arm64 and linux/amd64
containers. Step, Snapshot and Restore now take about 1.3, 1.9 and 2.2 times
as long, measured on a loaded machine: the cost of the checks.
Co-authored-by: Hanzo Dev <[email protected]>
go/ is the module github.com/hanzoai/dev/go. It embeds the module `make wasm`
builds and runs it under wazero through hanzoai/wasm, so cloud drives the core
itself, with no cgo and no Rust toolchain.
Open compiles once and refuses a module whose dev_abi is not 1. New and
Restore each start an instance with 64 MiB of its own; Step and Snapshot cross
into it; Close drops the session and the instance. The out-parameter is one
twelve-byte loan per session, every input loan is released and every dev_buf
freed on error paths too, and ten thousand steps leave the memory exactly the
size it was. dev_free takes its dev_buf by value, which the wasm32 C ABI passes
as one pointer: a test pins that signature and the other eight, and that the
imports are the four WASI calls and nothing else.
dev.h's statuses are sentinel errors. A trap answers ErrPanic, then ErrPoison,
for its session alone: a 24 MiB prompt makes the allocator refuse inside serde,
and the session beside it finishes its turn. A call after Close, or after the
Core closed, answers ErrHandle; a call while another runs answers ErrBusy.
The types are crates/protocol: an enum is a struct of its variants with exactly
one set, and the encoder refuses any other count. Every type goes through the
module itself, and Go writes the core's answers back byte for byte. Breaking
the host thirteen ways, one at a time, failed a test each time.
`make wasm` copies the module to go/dev.wasm without DWARF and with the cargo
home remapped, so the bytes do not depend on where they were built: 320,088
bytes, the same from another worktree and another CARGO_HOME.
Co-authored-by: Hanzo Dev <[email protected]>
551e180a1f said the refused-allocation test stays native, and nothing kept it
there: under Miri the isize::MAX-class request is Miri's own resource
exhaustion, which ends the run instead of answering. The attribute says so, so
`cargo miri test -p dev-ffi --lib` now finishes: 20 passed, 1 ignored.
Co-authored-by: Hanzo Dev <[email protected]>
A host with no memory of its own borrows its out-parameters from dev_alloc, and
the library writes a u64 handle or a Buf into them in place. The loan was laid
out as bytes, alignment 1, so where those writes landed was up to the allocator.
Every allocator this builds against today over-aligns — wasi-libc hands back 16
in dev.wasm, malloc 16 natively — which is why nothing showed. The contract
never said so: Miri puts a one-byte loan at an odd address, and the crate's own
borrowed-buffer test is a misaligned write under it, on main as well as here.
dev_alloc and dev_release now work one layout out from len, aligned for a u64
and a Buf alike, so the promise is the library's and the pair still agrees
about what is freed. dev.h says what a loan is aligned for.
The test checks every length from 1 to 64. It passes natively with or without
the fix, because malloc is generous; under Miri it fails without it ("1 bytes
were lent at 0x1bdcdd") and passes with it, along with the 19 other ffi unit
tests Miri can run. The wasm import list is unchanged.
isize::MAX bytes have no layout once rounded up to that alignment, so the
refusal test and the C fixture ask for the longest length that still has one:
it has to be the allocator that says no for either to mean anything. Handling
that refusal the infallible way aborts both. That test stays native — Miri
cannot model an allocator that refuses, and gives up on the request instead.
Co-authored-by: Hanzo Dev <[email protected]>
A snapshot's checksum catches a store that lost or changed bytes. It says
nothing about whether the sequence the snapshot restores agrees with the host's
journal, and a snapshot older than the journal mints again ids the host has
already dispatched under — the one way to run an effect twice that the id ledger
cannot see from inside.
That warning lived only on pack(). It is now at the three doors a host author
opens to restore a session — Session::restore, dev_restore and dev.h — each
pointing back to pack() for why the check is the host's.
Co-authored-by: Hanzo Dev <[email protected]>
Cancel carried no id and stopped whatever turn was in flight when it landed.
Under at-least-once delivery that is the wrong turn: Turn 1, Cancel, Turn 2,
and the first Cancel delivered again answered Done{turn: 2, Cancelled} for a
turn nobody cancelled. Every other event was already safe to replay — a turn by
the high-water mark, a result by the outstanding ledger and its family, a timer
by the saved counter — so "for every event this protocol has" was false for
exactly this one.
Event::Cancel carries the turn id. The core acts on it only while that turn is
in flight or queued, and a turn id is accepted once, so a redelivered cancel
finds nothing under its id. In flight, the turn ends and takes the queue behind
it along, as before: a stop that started the next prompt would not be one.
Queued, the prompt goes alone, with a Save for the queue and its own Done. Any
other id changes nothing, and a cancel that arrives ahead of its turn does not
spend the id.
One event's bytes change: "Cancel" becomes {"Cancel":{"turn":1}}, spelled in
the C fixture. A host still sending the old form is refused with DEV_MALFORMED
rather than misread. ABI stays 1, as it did when Turn gained its id: nothing
has shipped under it, and a snapshot holds no event.
Three tests pin it. With the id check taken out they all fail, the first on
the redelivery's [Save, Done{turn: 2, Cancelled}].
Co-authored-by: Hanzo Dev <[email protected]>
dev_alloc built its buffer with vec!, which allocates on the path that cannot
fail: a valid layout the allocator refuses goes to handle_alloc_error, and that
aborts. An abort is not a panic, so the catch_unwind around it never ran.
dev_alloc(isize::MAX) died on SIGABRT, taking every other session in the
process with it, and the same call into dev.wasm trapped on unreachable — under
a doc and a header that both promised null.
Ask the allocator directly. Layout::from_size_align refuses a length with no
layout, alloc_zeroed answers null when there is no memory, and nothing left in
there can panic, so the catch_unwind goes. dev_release frees the buffer as the
boxed [u8] whose layout this is.
The module doc and dev.h now say how far "the process stays up" reaches: a
panic, and no further. Memory the library needs for itself still aborts when
refused — with the module's memory lent out, a dev_step on a 512 MiB input
traps — and dev_alloc is the one place a refusal comes back.
The test asks for isize::MAX bytes and the two lengths past it; against the old
body it kills the test binary with SIGABRT, and so does the C fixture asking
for PTRDIFF_MAX through dev.h. The wasm import list is unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
The C ABI builds for wasm32-wasip1 unchanged: the seven dev_* exports and the
module's memory. A Go host can then run the loop with no cgo and no Rust
toolchain in its own build, and a panic is a trap the host survives rather
than something that has to be caught before it crosses a C boundary.
One thing was missing. Compiled to wasm the library's memory is its own, so a
host has no way to produce an input pointer or an out-parameter — every one
has to name memory inside the module. dev_alloc lends it zeroed bytes there
and dev_release takes them back. A boxed slice rather than a Vec, so the
allocation is exactly the length asked for and there is no capacity for a
host to remember. A native host shares an address space and calls neither.
`make wasm` builds the module now; it used to build protocol and core alone,
on the premise that ffi was native by definition.
What the module IMPORTS is the proof that the core touches nothing:
environ_get, environ_sizes_get, fd_write and proc_exit, and no file, socket,
clock or random source.
Measured from a wazero host: compile 85ms once, instantiate 46us, a step
86us, a snapshot 21us, about 1 MiB a session. A first turn answers
[{"id":1,"op":{"Model":{...}}}] — the model ask, carrying its id.
Co-authored-by: Hanzo Dev <[email protected]>
inside() is lexical, which is the whole of what a crate with no filesystem
can say: a symlink inside the workspace that points out of it resolves
outside it, and only the host holds the tree to see that. And name what a
DEV_MALFORMED on the way out would mean, since the arm is dead for types
made of integers, strings, options and enums.
Co-authored-by: Hanzo Dev <[email protected]>
observed() and answered() removed the outstanding entry before comparing the
family it was dispatched to, so a result from the wrong family — refused,
answering with nothing — deleted the entry the real result needed: the turn
wedged forever, or resumed early and asked the model with an effect still in
flight. Compare, then remove.
Message becomes User, Agent{text, calls} and Tool{id, text, failed}, so the
transcript keeps the call each result answers instead of throwing it away; a
provider that needs the assistant tool call ahead of the result can be driven
from this state. The prelude moves to Ask, where a system prompt belongs.
A Turn carries the host's id, and the core refuses one at or below the highest
it has accepted, so a redelivered prompt runs once. A Timer asks for a Save only
when the sequence has moved. Done names the turn that asked, a prompt queued
mid-turn answers with a Save, and a cancel says what became of each — so N
prompts produce N Dones and nothing is accepted in silence.
A path in a call is workspace-relative; the core refuses one that is absolute or
climbs out rather than leaving every host to confine /etc/passwd itself. Patch
names the one file it edits. What may run and what may be fetched stays policy,
and policy is /v1/dev's.
A snapshot carries its ABI and a checksum of its body, so a truncated, foreign
or edited one is refused rather than restoring a session that re-mints ids the
host has already dispatched under. It is not a signature: there is no key here
and no identity.
Slab::put swallowed a failed lookup, so a dev_drop landing inside a step
answered DEV_OK and dropped the session it had just stepped. put hands the value
back, enter answers DEV_HANDLE and lends nothing out, and remove keeps a slot
whose value is on loan out of the free list until the call lets go. encode
returns a Result instead of panicking inside a library, and the doc no longer
claims dev_abi runs inside catch_unwind.
43 tests, and each fix is pinned: putting the defect back fails the test that
names it, 14 for 14.
Co-authored-by: Hanzo Dev <[email protected]>
zap-proto is published and is a runtime for generated code; the generator
needs the zap schema compiler, which this repo does not build with.
Co-authored-by: Hanzo Dev <[email protected]>
protocol carries the events and the ten actions, and every action carries
the id its dispatch and completion are recorded under. encode and decode
are the only two functions that know the byte format, so ZAP takes over
from serde_json without moving a type or a symbol.
core is a Session with step, snapshot and restore, and no effects at all:
no filesystem, no process, no socket, no runtime. codex-core is not
separable as it stands — tokio, an HTTP client, a rollout store, a pty —
so the model-request and tool-result loop is driven here, and the module
doc names what stays coupled upstream. An observation is accepted only
while its id is outstanding, so a replayed result runs nothing twice.
ffi is the C ABI: seven symbols, generational handles so a stale one is
refused rather than resolving to the next session, catch_unwind on every
entry point so a panic poisons one session instead of aborting the
process, and dev.h written beside it. A C program exercises the archive.
protocol and core build for wasm32-wasip1; make wasm is the check.
Co-authored-by: Hanzo Dev <[email protected]>
typescript 7.0.2 is the native compiler and exports no JavaScript API, so
ts-jest, tsup's dts build and typescript-eslint cannot read it. It comes in
aliased as tsgo and does the typechecking through new typecheck scripts;
typescript stays on the 6 line for the tools.
shell-tool-mcp asked for moduleResolution "Node", which TS7 removed, so it
moves to node16 alongside its module; the node and jest globals it uncovers
are now declared rather than swept in. 30 diagnostics before, 29 after, the
same on both compilers.
Co-authored-by: Hanzo Dev <[email protected]>
Release and bump commits were authored by the automation address, so the work never reached the owner's contribution graph.
Co-authored-by: Hanzo Dev <[email protected]>
Release / darwin-amd64 (push) Blocked by required conditions
Release / darwin-arm64 (push) Blocked by required conditions
Release / linux-amd64 (push) Blocked by required conditions
Release / linux-arm64 (push) Blocked by required conditions
Release / windows-amd64 (push) Blocked by required conditions
Release / publish (push) Blocked by required conditions
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.
One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.
Co-authored-by: Hanzo Dev <[email protected]>
The first cut died at `curl` exit 22 with nothing about the cause: `set -e`
took the run down before the message meant to explain it, and the step also
reached for jq in the one job that provisions no toolchain.
Now each failure names itself — an empty client id, the HTTP code KMS answered
(body redacted), a login with no accessToken, or no readable token on any path —
and the JSON is read with sed, which is present before anything is installed.
Co-authored-by: Hanzo Dev <[email protected]>
The forge org carries KMS_CLIENT_ID and KMS_CLIENT_SECRET and nothing else, so
secrets.GH_PAT expanded to empty and every call left as
Authorization: Bearer
GitHub answered 401, the draft release was never opened, and all five platform
jobs and publish skipped behind it. Nothing has shipped since v0.6.106 on
September 2 — a release train stopped by a header with nothing after the space.
The publish job already reads NPM_TOKEN from KMS; the GitHub token now arrives
by the same road, in the three jobs that need it, and a token KMS will not hand
over fails the run loudly instead of expanding to nothing.
Co-authored-by: Hanzo Dev <[email protected]>
logout cleared the saved Hanzo key whenever the file existed, so signing out
of ChatGPT deleted a Hanzo key the user pasted weeks ago. The two credentials
are separate accounts; destroying the one nobody asked about is a surprise
nothing undoes. The gate caught it:
assertion failed: home.path().join("hanzo-api-key").exists()
logout_removes_only_the_selected_providers_saved_key
The active provider now decides which credential goes. The Hanzo side still
runs FIRST, because run_logout exits the process and anything sequenced after
it is unreachable — the defect that once printed success while leaving the
credential readable.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/ci moved the reusable to .github/workflows/build.yml — the one path
both planes resolve, since github.com reads a reusable only from there and the
forge reads it too. The v1 tag moved with it, so every caller still naming
.hanzo/workflows/build.yml asks for a blob that is not in the tree:
PrepareRunAndInsert: InsertRun: read hanzoai/ci@v1:.hanzo/workflows/build.yml:
object does not exist
Ninety-five repos still name the old address; this is one of them. The caller
stays at .hanzo/workflows/cicd.yml, which is deliberate — github.com has no
runner for these labels, so a caller placed there could never be scheduled.
Co-authored-by: Hanzo Dev <[email protected]>
Four surfaces still printed the upstream product. Found by running every
subcommand and reading the output rather than by grepping the sources, which
is why they survived the earlier sweep: three live in help text that only
clap assembles, and one is a JSON key.
--profile $CODEX_HOME/<name>.config.toml
--ignore-user-config $CODEX_HOME/config.toml
doctor "rerun codex doctor", "CODEX_HOME was resolved"
doctor --json "codexVersion"
sandbox the sandbox-state capability name
$CODEX_HOME was not only off-brand, it was wrong: initialize reads DEV_HOME and
writes CODEX_HOME, so a reader who followed the help and set CODEX_HOME had it
overwritten. The name that is ours is also the one that works.
doctor --json is a machine surface, so its key is a contract; both report
shapes carry it and both are renamed. The sandbox capability is a wire
identifier and keeps its value — only the sentence describing it changes.
Co-authored-by: Hanzo Dev <[email protected]>
`dev login` opened a browser for ChatGPT and had no way to reach the account
the tool actually runs on. It now offers the four credentials that exist —
Hanzo account, ChatGPT account, or a pasted key for either — with Hanzo first
and selected, because Hanzo is the provider the default config names.
A pasted key lands in the profile's own file, written through a temporary file
so a failed write cannot truncate a working credential, and readable only by
its owner. It is separate from auth.json, which the Hanzo tools share.
Logout was the defect worth the most here: `run_logout` exits the process, so
every statement after it was unreachable. On the default provider the Hanzo
credential was never cleared at all, and the command still printed success —
a logout that left the account readable. The Hanzo side is cleared first now,
on every path, and a failure on one side no longer stops the other.
Activating the Hanzo provider also writes the provider table when the profile
lacks it, so switching to Hanzo in an older profile no longer names a provider
that is not defined.
Co-authored-by: Hanzo Dev <[email protected]>
Adding `submodules: true` to the checkout step left two jobs with two
`with:` blocks. GitHub's workflow parser rejects a duplicate mapping key
outright, so the release lane could not be scheduled at all. It passed
review here because `yaml.safe_load` keeps the last duplicate silently —
the check now refuses duplicates the way GitHub does, and runs over all
three workflow files.
The --strict-config anchor started mid-clause, so the help it produced
read "fields that are this version of Dev does not recognize". It takes
the whole clause now. The branding test did not catch this because it
greps for the upstream name and never for coherence.
The branding check read `dev --help` only, which is why `dev exec`
announced "OpenAI Codex" as its banner for a while — `--help` never shows
it. It now reads every subcommand's help, and found four more leaks by
itself: exec's usage line, its --strict-config and --skip-git-repo-check
text, and `-c/--config` still naming ~/.codex/config.toml. That last one
was recorded as unreachable when a copied island could not include
utils/cli; editing the checkout in place made it reachable.
Two things keep the word legitimately and are exempt: CODEX_HOME, an
environment variable upstream owns, and `codex/...`, which names a method
on the app-server wire protocol. Renaming either breaks something real.
Branding is the product name a reader sees, not identifiers on a wire.
hanzo-upstream had no tests, and it is the code that edits upstream in
place — including an idempotence bug shipped earlier today that stacked
appending edits until `hanzo-tui` appeared three times in one manifest.
The anchor logic is now a pure function with six tests, one of which is
that exact regression, and one of which asserts every shipped anchor is
well formed. `make test` reaches the crate now; being a detached
workspace, the root run could not see it.
Cloud tasks are Hanzo Tasks, not the upstream service's name.
`codex-cli/` published `@hanzo/dev`, so the directory carrying our npm
package was named after someone else's product. It is `npm/`.
`patches/codex.json` is `patches/upstream.json` — it anchors edits against
upstream, and the file should say so rather than name one of them.
Deleted outright: `justfile`, whose first line was
`set working-directory := "codex-rs"` for a directory this repository has
never had; `announcement_tip.toml`, an example file for a TUI we do not
ship; and `.codex/` and `.code/`, which held upstream's own process skills
— babysit-pr, codex-bug, pushing-ci-changes — that were never ours.
Three branding leaks a real run turned up, none of which the help text
showed: `dev exec` announced "OpenAI Codex v0.7.0-dev.1" as its first
line, the status card was missed when the session header was anchored,
and both bubblewrap warnings said "Codex's Linux sandbox".
README: say what ships. It claimed the agent drives a browser, which is
the port that has not landed, and showed a screenshot of it doing so.
Both are gone until the bridge is back. The model count is "hundreds" —
`/v1/models` answers 458 today, an earlier line here said "over 500", and
hanzo.ai derives its own count precisely because typed ones drift.
Document the npm install beside the curl one; both give the same build.
The product home was `~/.hanzo/dev2` — named after the migration workspace,
which is now an archived repository. It is `~/.hanzo/dev`.
`~/.hanzo/auth.json` is the credential. Dev links its own auth.json to it,
so signing in to any Hanzo tool signs in to all of them while dev keeps its
own sessions and settings. A credential the user already has is left alone
rather than replaced by a link, running it twice changes nothing, and an
explicit DEV_HOME is a separate profile with its own account — each of
those is a test, because each is a way to lose someone's login.
LLM.md still described code-rs, build-fast.sh and a VT100 harness under a
directory this repo no longer has — and AGENTS.md, CLAUDE.md and GEMINI.md
all symlink to it, so it is the file every agent reads first. Rewrite it
for the workspace that is actually here, including the rule that upstream
is never edited by hand because `make bump` discards such edits.
Fix five broken images: the README pointed at docs/logo.png and
docs/screenshots/, and they live in docs/images/. Predates the cutover.
State the catalog honestly. "enso and the zen5 family" undersold a gateway
that answers for over five hundred models. Round, not exact — hanzo.ai
derives its count from the pricing snapshot and fails the build on a
hand-typed one, and a number typed here would drift the same way.
Replaces the Rust tree. `code-rs/` was a fork of just-every/code carrying
its own copy of everything; `vendor/` held 219 MB of upstream snapshots
beside it. Both are gone.
In their place: Hanzo writes the command surface, upstream keeps the
engines. `crates/hanzo-dev/src/main.rs` is a real file that dispatches
into libraries — codex_tui::run_main, codex_exec::run_main, and the rest
— rather than an include! of upstream's 5156-line binary main. Owning the
front door is what makes a single unified command surface expressible.
Upstream arrives as submodules at upstream/codex and upstream/code, both
at latest. The gitlink is the pin, so no lockfile of hashes sits beside
it and a bump is a two-line diff instead of forty-five thousand. Crates
are consumed by ordinary path dependency; nothing is copied.
The whole Hanzo delta on upstream is sixteen anchored substitutions in
patches/codex.json, applied in place by `make prepare` and visible in one
`git -C upstream/codex status`. Each carries an exact match count, so
upstream moving the ground under one stops preparation and names the file
rather than branding the wrong line. Two are upstreamable: they publish
mcp_cmd, plugin_cmd and doctor from upstream's library instead of burying
them in its binary.
`dev` covers exec, login, logout, serve, resume, fork, review, agents,
queue, archive, unarchive, delete, cloud, mcp, plugin, doctor, features,
sandbox and completion. Auto Drive and the browser bridge are not ported
yet; migration/STATUS.md records that, and the defects the v1 patch was
already carrying, so they are fixed as features are ported rather than
copied across.
Releases build from the root after `make prepare`, with submodules
checked out. Scripts that existed only to manage a fork-diff — the
upstream-merge set and the codex path-dep check — go with the fork.
Mirror openai/codex c308ffb36 exactly under vendor/codex. Apply the code-rs delta from just-every/code 7d59424890 to af789f51a, retaining Hanzo defaults, authentication, branding, and vendor paths. Give the compatibility code binary its own entry file to eliminate duplicate-target warnings.
Validated: BUILD_FAST_BINS="dev code" ./build-fast.sh, both binary versions, shell completion, and interactive Hanzo TUI startup.
Ranges drift once nothing moves them: a caret that has not been rewritten
since it was written pins whatever was current that day, and an exact pin
cannot even take a patch. Both are updated here.
One line per package, so a surface reads the same components, the same
tokens and the same face as every other one.
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.
Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.
Comments naming a face we no longer use are deleted, not reworded.
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.
Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
runs-on was a bare scalar. Jobs written that way queue and are never
picked up — link and quest have waited since 10 August, painter since the
5th, none of them with a single success — while repos writing the same
label as a sequence drain within seconds of a push on the same fleet.
Same label, same runners, different shape. It fails silently: nothing errors,
the job simply never starts, so the lane looks configured and does nothing.
Every dependency on a @hanzo/@luxfi/@zooai package now names the same version
across the repo: a catalog entry where pnpm supports one, an exact pin where it
does not, and a range only where a peer genuinely means one.
pnpm itself is pinned too — the estate ran eighteen different versions, five of
them too old to read a catalog.
Pydantic omits `required` entirely when every argument is optional,
which is how FastMCP writes most of a Python server's tools. Our Object
serializer read an absent list as "every property is required", so the
model was told `agent` needs all fifteen of its arguments and `mpc` all
seventeen — 17 of the Hanzo server's 95 tools, measured by running their
live schemas through the converter. JSON Schema reads an absent list as
requiring nothing, so write that list where a server omits one.
While in the same resolver: measure a scalar `type` against the same six
types the union path already measures against, rather than against
`!= "null"`. The list is named once and read by both, so they cannot
disagree about what we can hold, and a type outside it degrades to an
accept-all string instead of failing the whole tool it sits in.
The version job picks the next patch above the newest tag, the newest npm
package and package.json. All three of those appear only when a run
publishes, about twenty-five minutes after it starts. A second run that
begins inside that window sees the old numbers and picks the same version,
cross-builds five targets, and dies on `npm publish`:
npm error You cannot publish over the previously published versions: 0.6.101.
That is exactly what happened to v0.6.102. The draft release the first run
opened at second zero was already the reservation; nothing read it back.
Now it counts, so the window closes to the moment between reading and
opening a draft rather than the length of a whole build.
`concurrency: release` does not serialize these runs — two were in flight
at once, which is how the overlap arose in the first place.
Co-authored-by: Hanzo Dev <[email protected]>
postinstall downloads a binary from a release and executes it. Until now
the only check on those bytes was the file magic — proof that they are an
executable, not proof that they are ours. Every release asset already
ships a sibling .sha256; read it and compare.
The per-user cache is keyed by version number alone, so a binary of
unknown provenance sitting under an already-cached version survived every
reinstall. A cache entry is now trusted only while a stamp beside it
still matches its bytes, and the stamp is written after the published
checksum verified the archive it came from — so a cache hit carries that
proof forward.
Verified against v0.6.100: a tampered cached binary is refused,
re-downloaded, checked and repaired.
Co-authored-by: Hanzo Dev <[email protected]>