Commit Graph
91 Commits
Author SHA1 Message Date
zeekay 3a7f9efa09 fix(core/tools): an MCP schema without required requires nothing
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.
2026-08-21 12:03:30 -07:00
zeekayandHanzo Dev 2a026abecb fix(cli/doctor): name the formula, package and binary that exist
doctor is where a user with three installs goes to find out which to
remove, and it was telling them `brew uninstall code` — a formula that
was never published — and to "prefer 'coder'", a name that is not the
canonical one. It also announced itself as "code version".

The guidance now lives in one value so a test can hold it, the way
hanzo_identity.rs holds the defaults. Prose drifts; a value with a test
does not.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-19 23:18:37 -07:00
zeekayandHanzo Dev 06d28475d6 tui: sign-in leads with Hanzo, and the waiting screen holds still
A vendor key exported in the shell picked the provider implicitly, and
the onboarding screen took that as identity: it greeted a fresh user
with "Sign in with ChatGPT" and offered their ANTHROPIC_API_KEY back to
them. An inferred provider is not a decision. Sign-in now leads with
Hanzo IAM unless a provider was NAMED (flag, profile or config.toml),
a completed Hanzo sign-in flips the session onto the account it just
created, and a stored Hanzo session outranks a stray vendor key in the
next launch's implicit pick.

The sign-in waiting screens also stop shimmering: each render scheduled
another frame in 100ms, redrawing the whole terminal ten times a second
for as long as the login took — the "spazzing". A waiting screen is now
a still line, and the shimmer module goes with its last caller.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-17 15:13:31 -07:00
zeekayandHanzo Dev d7a7da530b release: thin LTO — five targets on one Mac, not five hours
Fat LTO with one codegen unit spent ~30 minutes linking each of the five
release targets, so a release took the runner most of three hours. Thin
LTO builds the same target in a third of that for a few percent of size.
release-prod keeps the fat profile for a deliberate small build.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 16:38:47 -07:00
zeekayandHanzo Dev c176bf737b tui: the plain look — no header, no frame, no gutter
The chat surface is the one the 0.6.8x builds had: `>_ hanzo dev (vX)`
and the question, the transcript flush against the edge, the composer a
line with its running status on the row above, "What would you like to
work on?" as the greeting. The status bar, the composer frame, the
gutter and its icons, and the block-letter splash are gone; the merge
that brought them in is what this walks back, on top of the current
TUI.

While here: the free-pool notice is decided by the configured run — the
Hanzo provider on the pool's model — not by which key happens to be in
the environment, so a run against another provider no longer announces
itself as free; the trust screen names Hanzo Dev, not Code.

Snapshots follow the look. Tests that read the gutter for meaning now
read the record (mid-turn state) or the exec label ("Running..."), and
the spacer hysteresis test derives its viewport heights from the
transcript instead of assuming the tall splash.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 15:59:31 -07:00
zeekayandHanzo Dev 730e22c429 core: keep MCP tools whose optional arguments carry a null arm
The Hanzo MCP server spells every optional argument as
`anyOf: [T, {type: null}]`, and the schema sanitizer let the null arm
through to a parser that only knows six types — so `hanzo code` attached
twenty tools and dev dropped every one at the door with "unsupported JSON
schema type null". The null arm says nothing about a value the model may
send; drop it, keep T, and a bare `type: null` reads as a string.

The coder prompt now speaks as Hanzo Dev rather than a community fork, and
says how an app starts: find the open-source project to fork first, then
write only what does not exist.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 14:58:40 -07:00
zeekayandHanzo Dev 6c7c3d3af4 release: one job on the Mac runner ships all five targets
The GitHub Actions lane was dropped and nothing replaced it, so v0.6.95 was
tagged and never published; npm and every installer still hand out 0.6.94.
The reason given — no runner for four of the five targets — is not true of a
Mac: it builds darwin natively, both linux-musl through cargo-zigbuild and
windows-msvc through cargo-xwin, all verified from this machine. So the
release is .hanzo/workflows/release.yml on the dbc-metal host runner, one
job, every push to main: build, GitHub Release + forge release, npm, Homebrew,
then the bump commit and tag. hanzo_identity guards the new file.

Two things found while proving it end to end:

- `dev login` on the Hanzo path died on a Cloudflare 502 page mid-poll —
  "unexpected token response: <!DOCTYPE html>". A body that is not an OAuth
  error is the edge, not the issuer; keep polling and let the device code's
  lifetime end it.
- The troubleshooting link in agent_tool still pointed at just-every/code.

Merges the forge's 0faa6fe so both remotes share one history; the blob-size
gate it ported into hanzo.yml stays.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 14:51:31 -07:00
zooqueen d70acafded dev answers without an account, on the free pool
Installing dev and running it got you a sentence telling you to go get a key.
The gateway will answer without one — the free pool costs nothing to serve — so
the first run now asks it instead of refusing.

The credential and the model move together, because either alone is broken: the
free key may only ask for a route that costs nothing, and a coding model asked
with it is refused. `free` is the pool's own id rather than a vendor `:free`
one, so the routes underneath can be retired without reaching this binary.

An account still wins wherever both exist, and a build with no free key behaves
exactly as it did before — the refusal it used to give is what remains when
there is nothing to ask with.

The TUI stopped offering to sign the user in to OpenAI on the way past. It was
reading auth.json alone, so a Hanzo bearer — the one `hanzo code` sets on the
child, and now the free one — looked like no credential at all.
2026-08-14 23:02:45 -07:00
zeekay 6dbfa82403 the key you already hold picks the provider
A coding agent should work with whatever credential is in the environment
rather than insisting on ours. It insisted: with no explicit choice the
provider defaulted to hanzo unconditionally, so someone holding only an
ANTHROPIC_API_KEY was told to go and get a Hanzo key before anything would run.

provider_from_env answers only after a flag, a profile and config.toml have all
declined — it is the last link in that chain, never an override. Hanzo first,
because it is the default and the one we serve; then a key that names its own
vendor. An empty or whitespace value is not a credential.

With NO key the answer is still hanzo, deliberately: that is the provider whose
env_key_instructions say how to sign in, and an empty environment is exactly
when someone needs to read them.

Anthropic was not reachable at all — zero mentions in the tree. It is built in
now through its OpenAI-compatible surface, so it needs no new wire format: a
base URL and a key. The native Messages API is a different shape and is
deliberately not spoken.
2026-08-08 09:19:01 -07:00
zeekay eb49cac620 name the env after the product, and the key after the stack it opens
457 sites carried an upstream prefix into every surface a user reaches — shell
exports, docs, CI, the SDK. CODEX_ is not our name and it was never going to
stop arriving on its own: it is what upstream writes, and every sync brings more.

Two namespaces, because they answer different questions.

  DEV_*            the app's own configuration — DEV_HOME, DEV_PROFILE, the rest
  HANZO_API_KEY    a credential for the Hanzo stack, which is not app config

CODEX_API_KEY held the literal string "CODEX_API_KEY", so the rename moves the
value and not just the constant. It becomes HANZO_API_KEY, which is what
create_hanzo_provider already reads through HANZO_MACHINE_ENV_KEYS — one name
for one credential instead of two spellings of it.

vendor/ is untouched, which is the point of vendor/: upstream code keeps
upstream names, and the boundary is a path rather than a habit.
2026-08-08 09:14:56 -07:00
zeekay c42d954ea4 fix the two preflight failures I pushed
The Auto Drive routing test asserted the gpt-5.5/gpt-5.4-mini defaults I had
just replaced, and the sync moved app-server-protocol/v2 without regenerating
the vendored schema fixtures, so MultiAgentVersion.ts was missing.

Both were visible only by running the suite. I ran hanzo_identity and the TUI
tests and pushed on that, and `make check` compiles test targets without
running them — neither is the gate CI applies.
2026-08-07 22:56:51 -07:00
zeekay 57ca4264e2 core: a struct field is not a statement — drop the one that landed in a fn body
The regex that added alt_env_keys matched `ModelProviderInfo {` at the end of a
line, and that spelling occurs twice: the struct literal, and the SIGNATURE of a
function returning one. So `fn provider_for(...) -> ModelProviderInfo {` got a
struct field as its first statement, and the same test target failed again — a
different error at the same address, which is easy to read as "the fix did not
land" rather than "the fix landed badly".

I also let a local `cargo test --no-run` be killed by my own timeout and pushed
without it. The compile was the check, and I shipped the unchecked version.
2026-08-07 22:00:19 -07:00
zeekay e45a1532a5 follow the vendored tree to its new path
The move to vendor/ left four references behind. check-codex-path-deps.sh
hard-failed every build ("Expected both code-rs/ and codex-rs/"), and the
auto-review scope excluded a prefix that now matches nothing — so all 5,624
vendored files were eligible for review context. That exclusion is vendor/
now, which covers whatever else we vendor later.
2026-08-07 21:52:03 -07:00
zeekay b3de714dec auto drive: point at models the Hanzo Cloud serves, and guard it
Auto Drive defaulted to gpt-5.1 and routed between gpt-5.5 and gpt-5.4-mini.
api.hanzo.ai answers all three with 400 model is not available, so on the
default provider the feature was not mis-labelled — it was broken.

The seed also read auth mode alone. A Hanzo device login persists as
AuthMode::ChatGPT, so it handed our own gateway an OpenAI id; the provider
decides now, matching the rule the default model already followed.

hanzo_identity gains three assertions, because the identity table it guards
was never the thing that regressed — rendered text was, and nothing looked
at it. Each one was checked by breaking the code it guards and watching it
go red: the Homebrew formula, the Auto Drive defaults, and the first screen
a new user sees.
2026-08-07 21:51:09 -07:00
zeekay 23b29ae484 model: offer the models we actually serve, in the order we mean
/model listed seventeen OpenAI presets and no Hanzo one, so the model dev
runs by default was not selectable in the view whose job is selecting it.
Adds the Zen and enso lineup from the catalog — ids and context windows read
from /v1/models, not guessed.

Two things had to go first, both of which re-derived order from the slug:

model_selection_view parsed a version out of `gpt-N.M`; everything else fell
to a branch that sorted it below every OpenAI model, so declaring zen5-coder
first would still have placed it sixth. chatwidget hoisted a hardcoded
five-slug shortlist to the front regardless of catalog order.

Both deleted. Order is the catalog's job now, decided once, and a test holds
the picker to leading with our default. The OpenAI presets stay: they serve
the openai provider as its offline fallback and for effort clamping.
2026-08-07 21:50:59 -07:00
zeekay 383f8e92a9 login: sign in to Hanzo, over the standard device grant
The default provider is hanzo, yet the first screen offered a ChatGPT plan
and the only device flow spoke OpenAI's proprietary /api/accounts/deviceauth.
This ports the RFC 8628 device grant into code-rs — the workspace that ships;
the earlier attempt landed in the vendored mirror, where it ran for nobody.

Routing keys off model_provider_id, not a new flag: the config already
carries the one notion of provider. openai keeps device_code_auth.rs
untouched, and every branded string is provider-conditional, so that path
renders exactly what it did before.

The client is hanzo-cli. hanzo-app answers "client authentication failed" —
it is confidential, and a CLI cannot hold a secret. Sibling brands need a
public <org>-cli client provisioned before the issuer override is useful.

One row, not two: the Hanzo browser loopback is not provisioned
(invalid redirect_uri), and a row promising hanzo.id that opened
auth.openai.com would be a lie. The device grant covers headless anyway.
2026-08-07 21:50:49 -07:00
zeekay ad27f9279c tui: say our own name where the product speaks
`brew upgrade code` names no formula — ours is hanzo-dev — and the TUI ran
that string, it did not merely print it. The rest is copy the product says
about itself: the approval line on every command, the update pane, /limits,
the theme and spinner pickers, `dev exec --help`.

The model-migration prompt kept its mechanism, since our own catalog can
declare a successor through it, but loses the GPT marketing and the three
openai.com links it showed on first run.

Dropped format_model_name: it rewrote only `gpt-` slugs, so for a Hanzo
model it was a no-op wrapped around someone else's brand. The status line
now shows the model id the user configured — the only name we can vouch for.
2026-08-07 21:50:38 -07:00
zeekay 25f1f1da4f vendor: upstream lives under vendor/, so ours is what is left
codex-rs/ is a read-only mirror of openai/codex that just-every refreshes and
we never edit. It sat at the repo root next to code-rs/, the product, and the
only thing separating them was that everyone knew. Nothing checked it.

That knowing is what makes branding endless. There are ~400 upstream-brand
literals in code-rs today and ~224 of ours, hand-placed on the same lines the
sync delta lands on, so every sync re-fights the same 17 files. You cannot fix
that while the line between "theirs" and "ours" is a convention.

vendor/codex/ makes it a boundary. Everything under vendor/ is upstream: never
branded, never edited, replaced wholesale on sync. Everything outside it is
ours and must carry our name — which is now a question a test can ask.

code-rs coupled to the mirror through exactly one line, an include_str! of
models.json, and it still does. The other 17 files are path strings.

Syncing changes by one sed: just-every's delta names codex-rs/, so rewrite the
patch paths to vendor/codex/ before applying it.
2026-08-07 21:22:34 -07:00
zeekay 68d83e82e6 core: the four test literals that have failed three releases
alt_env_keys was added to ModelProviderInfo so a sandbox run can present a
machine credential instead of a human session. Three construction sites were
updated. These four are in TEST targets, and `cargo check` does not compile
those — so it printed nothing, I read nothing as clean, and every release since
has died in preflight on E0063.

A check that cannot see test targets cannot clear a change to a public struct:
literals must name every field, so ADDING one breaks every construction in the
workspace, including those only `cargo test --no-run` compiles.

Vec::new() rather than a Default on the struct — a #[serde(default)] field still
has to be written in a literal, and a Default would let the next new field land
silently in tests that should have been made to state it.
2026-08-07 21:21:19 -07:00
zeekay e58c22ffd2 sync: just-every/code 7d59424890 — upstream reaches us through them
63 commits since the recorded sync point, including just-every's own refresh
of the codex-rs mirror to openai/codex:main. 371 files, but 361 of them are
mirror; the product delta is 8 files in code-rs/ (remote model discovery,
client retry, theme persistence, rollout recorder) plus a package bump.

codex-rs/models-manager/models.json — the one mirror file compiled into the
shipping binary — is unchanged by this delta and already byte-identical to
upstream, so the mirror refresh is hygiene, not function.

Note for the next sync: generate the delta with `git diff --binary`. Two .zst
schema fixtures reject a plain patch, and git apply is atomic, so it rolls
back all 371 files while printing "cleanly" for each one.
2026-08-07 21:01:30 -07:00
zeekay 4fa6daf138 shot: test the contract with cloud, and stop trusting the caller's paths
The screenshot publisher landed in 3a72312dda, a commit whose subject is about
the Hanzo provider's machine credential and which mentions none of it. This
finishes it and writes down what it is.

Two things it was getting wrong, both found by mutating the code and watching
which test failed:

The payload promised workspace-relative filenames and there was nothing making
that true. A turn diff names files relative to the git root when there is one and
absolutely when there is not, so an agent run outside a repository would have
published the operator's home directory — and their account name — into a log the
whole org reads. `ui::named` relativizes against the workspace and falls back to
the bare filename for anything still absolute, which a diff can produce because
it can name a file the workspace does not contain. The old test asserted the
absence of "/home/..." over a fixture that was already relative, so it passed
whatever the code did; the new one starts from absolute inputs.

Nothing exercised the three calls to cloud. They are now driven against wiremock,
which pins the door (`/v1/s3/buckets/sessions/objects`, then the signed PUT, then
`/v1/agents/sessions/{id}/events`), the exact payload, and the part that actually
matters: the org bearer goes to the two API calls and NOT to the presigned URL,
because the signature is that URL's credential and our token would be a second
one handed to whatever host cloud signed against. Adding `.bearer_auth` to the
upload turns that test red, which is the whole point of writing it.

Also covered: a session id out of the environment reaching a URL path, refused
unless it is alphanumeric-plus-dash-underscore, so `../../admin` cannot retarget
the append at another tenant's session; and cloud's 422 refusal surfacing as an
error rather than being read as success.

LLM.md records the subsystem, since its arrival in the history is mislabelled and
the next reader has no way to find it from the log.
2026-08-07 14:29:17 -07:00
zeekay df7925cbcd core: give the test providers the alt_env_keys field they now need
dbb650041c added `alt_env_keys` to ModelProviderInfo and updated the handful of
constructors that ship, but not the twenty-two that only exist under #[cfg(test)].
The struct is exhaustive on purpose — adding a field should force every
construction to decide — so those twenty-two stopped compiling, and with them the
whole `cargo test -p code-core` binary. `cargo check` stayed green throughout,
which is why nothing noticed: the declared gate in hanzo.yml does not build test
targets, so a break confined to them is invisible to CI.

Every site gets `Vec::new()`, which is what the production Ollama and OpenAI
providers already say and what the field means for a provider with no machine
credential. No behaviour moves.
2026-08-07 14:28:59 -07:00
zeekay 3a72312dda provider: record the fleet measurement that motivates the machine credential
The change is a RESTORATION, not a new capability, and the running fleet proves
it. HANZO_ENV_KEY was introduced by d3d1dfa1d3 (2026-08-01), which is NOT in
v0.6.91 — and v0.6.91 is the binary in the sandbox image today.

Measured against that live pod, variables set one at a time:

  nothing set          no request is attempted at all
  HANZO_API_KEY only   the request goes out; IAM rejects the sentinel (400)
  HANZO_USER_KEY only  no request is attempted at all

So the deployed build reaches our gateway on the MACHINE variable and ignores
the human one. main reversed that with nothing noticing, because a missing
credential and an unread one are indistinguishable from outside — every service
caller in the fleet would have stopped authenticating at the next image rebuild,
silently.
2026-08-07 14:00:16 -07:00
zeekay dbb650041c provider: a service has no human session, so the Hanzo provider takes a machine credential
create_hanzo_provider() read exactly one variable, HANZO_USER_KEY — the bearer a
person's `hanzo auth login` fills in. No service has one. Measured in a live
sandbox pod (hanzo-sandboxes/blue-ms-e2), `dev exec` for a perfectly
authenticated caller reported:

  ERROR ... Authentication expired. Authentication required. Run `code login`

That caller held an IAM token. It was refused because the only credential the
provider would look at is the one that exists on a laptop and never in a pod.

So the provider now declares alt_env_keys — HANZO_API_KEY (an org key) and
HANZO_MACHINE_TOKEN (the bearer an IAM machine identity holds after the
client_credentials exchange) — and api_key() tries those BEFORE env_key.

MACHINE FIRST IS THE DELIBERATE PART. Where both are present the process was
started BY something, and the credential it was handed is the one that should be
billed and audited; a stale human key left in an environment must not quietly
outrank the identity the caller actually presented.

alt_env_keys is empty for every other provider, so nothing changes for them, and
it is skipped on serialize so no config file grows a field it does not use.

Two tests pin it: the provider must read both machine names, and HANZO_API_KEY
must be tried before HANZO_USER_KEY.
2026-08-07 13:16:33 -07:00
zeekay 418d57f390 feat(tui,cli): the product says Hanzo Dev, not Code
The complaint was that the CLI and TUI still look like codex. They did, but
almost none of it was the word "ChatGPT" — the shipping tree has 69 of those
and most must stay, because they name a real OpenAI account the user actually
holds and renaming them would lie about whose login it is. What made it look
wrong was our own chrome naming itself after someone else's product.

The startup splash was the biggest piece and the one a ChatGPT/Codex grep can
never find: the ASCII art spelled EVERY over CODE — just-every's brand, not
ours. It now spells HANZO over DEV in all four size variants. While rewriting
it, `shift_left(lines, 3)` turned out to strip the first three space characters
anywhere in a line rather than three leading columns; that was invisible while
every line began with three spaces, and HANZO's first row does not. Replaced
with art written at final column positions, and the three near-duplicate
per-size word blocks collapsed into one HANZO array plus a word_lines() helper.
Added two tests for invariants nothing enforced before: declared art height
matches the line count, and each variant fits its own minimum width.

Also deleted a branding band-aid. The /help renderer ran
`cmd.description().replace("Codex", "Code")`, post-processing help text at
display time, which meant any rename that only edited slash_command.rs would be
silently mangled and "Hanzo" could never appear. Removed it and fixed the
source strings instead.

Renamed, because they name us: terminal window title, the DEFAULT_TERMINAL_TITLE
const, the desktop notification title, the assistant's label in the undo preview
("Dev", to stay balanced against "You"), the welcome banner and onboarding copy,
five slash-command descriptions, the settings-overlay upgrade line, the model
migration prompts, the `dev --help` config-override text, and `dev mcp add`.

Kept, because they name a real service or a wire contract: every "Sign in with
ChatGPT" / "your ChatGPT account" string, OPENAI_API_KEY, chatgpt.com and
openai.com URLs, OpenAI model ids including gpt-5.1-codex-max, the ~/.code/ and
~/.codex/ config paths, all CODEX_*/CODE_* env vars, config keys, OAuth client
ids, and the internal Line::from("codex") sentinels that pair with the
is_title_line matcher and are never rendered.

One upstream announcement was reworded rather than renamed: "Codex just got an
upgrade: meet gpt-5.1-codex-max" now names the model instead of the product,
since the model is real and ours is not the product being upgraded.

Verified: cargo nextest run --no-fail-fast --locked -> 1673/1674 pass. The one
failure, code-core git_info::test_collect_git_info_with_remote, is this machine's
`[email protected]:.insteadof https://github.com/` rewriting the test's URL; it
passed on CI run 31143152415.
2026-08-06 20:58:49 -07:00
zeekay c12d1fb6ef fix(tui/tests): drop the snapshot cwd pin that broke five chatwidget tests
My merge of origin/main combined both sides of smoke_helpers.rs, and one of
them was wrong to keep. main pinned the harness cwd to a literal "/snapshot"
so the status header's `Directory: <cwd>` segment could not leak the recording
machine's checkout path into a snapshot. Right goal, wrong mechanism here:

- Redundant. This harness also sets CODEX_TUI_FORCE_MINIMAL_HEADER, which
  renders a header with no Directory segment at all. No snapshot in the corpus
  contains a path or a `Directory:` line — checked all 32.
- Harmful. "/snapshot" does not exist on disk, so the chatwidget tests that
  resolve sandbox write permission against the working directory concluded the
  workspace was not writable. That is `assertion failed: action.write` in
  auto_handle_decision_launches_cli_agents_and_review, plus
  background_review_observe_idle_renders_notice_and_post_turn_input_from_agent_result,
  coordinator_router_bypasses_slash_commands,
  esc_stop_auto_drive_keeps_background_review_non_blocking, and
  heavy_agent_churn_with_auto_review_keeps_bounds_and_typing.

Those are exactly the five that failed Preflight on run 31143152415, which
skipped the release job and is why 0.6.93 never published. Verified by removing
the override and re-running: all five pass.

Left a comment where the const was, so the next person who wants machine-
independent snapshots picks a directory that exists.
2026-08-06 20:58:30 -07:00
zeekay 5480a29f95 Merge origin/main: adopt the release-pipeline fixes, keep our TUI and CLI
origin/main carried five commits our sync branch had not seen, four of them
repairs to a release pipeline that had published nothing to npm since April.
Resolved eleven conflicts by combining rather than choosing, because on every
one of them the two sides were additive, not opposed:

- .github/workflows/release.yml — ours is the superset (npm-auth-check and
  release-notes jobs, and `cancel-in-progress: false`, which is what you want
  for a release; theirs said true). Grafted in their three real fixes: the
  ubuntu-24.04 pin (ubuntu-latest sat fifteen minutes unacquired on run
  31128398197 and took the release with it), and the "assert every architecture
  built" step, which is the actual cure for a green release that shipped a
  partial matrix. Dropped their inline OpenAI proxy step: our release-notes job
  already starts that proxy on port 5058, so taking it would have run two.

- code-rs/tui/src/chatwidget/smoke_helpers.rs — kept both sides. Theirs pins
  the snapshot cwd to /snapshot so snapshots stop embedding the recording
  machine's checkout path; ours forces the minimal header and test mode. The
  minimal header means the Directory line never renders, so the pin is
  belt-and-braces, but it is correct and cheap to keep.

- code-rs/cli/src/main.rs — kept ours whole. Both sides made the same semantic
  fix (derive the expected completion name from CLI_COMMAND_NAME instead of
  restating "_code"); theirs was only the tidier phrasing of it.

- the 8 vt100 snapshots — regenerated, not hand-merged, and the regenerated
  output is byte-identical to what this branch already had. The header they
  record now reads "Hanzo Dev" where origin/main's read "Code".

- codex-cli/package.json — took theirs (0.6.92); ours was a stale 0.2.190.

Verified: cargo test -p code-tui --test vt100_chatwidget_snapshot 43/43 pass;
cargo test -p code-core --test hanzo_identity 4/4 pass.
2026-08-06 20:02:42 -07:00
zeekay c0cac2c012 fix(app-server-protocol): regenerate the vendored schema fixtures
`schema_fixtures_match_generated` compares the checked-in fixture tree against
freshly generated output, and it was red before this sync: 0a1b157d2c added
QueueUserMessage to the app-server protocol without regenerating, so json/v1
and typescript/ were each missing QueueUserMessageParams/Response.

Regenerated with the crate's own writer:
  cargo run -p code-app-server-protocol --bin write_schema_fixtures

That closes the pre-existing gap and picks up what this sync added — the diff
carries both the QueueUserMessage definitions and 15 transparent_background
occurrences from the image-generation metadata backport.
2026-08-06 14:38:56 -07:00
zeekay 9b016bda1a fix(release): the npm publish has shipped nothing since April
@hanzo/dev's latest is 0.6.91, published 2026-04-01. Every release run
since has ended red without publishing, for two independent reasons.

1. actions/upload-artifact@v3. The pin carries a comment about
   git.hanzo.ai presenting as GHES, where v4 raises
   GHESNotSupportedError — true, but this file lives under
   .github/workflows/ and therefore runs on github.com, which now
   hard-fails v3 during "Set up job":

     This request has been automatically failed because it uses a
     deprecated version of `actions/upload-artifact: v3`.

   Not one step executed. Two hosts, opposite requirements, one file:
   v3 belongs only under .hanzo/workflows/. Both uploads and the
   download move to v4.

2. Default fail-fast. The aarch64 leg reached the deprecation check
   first and died in 4s, cancelling the other four targets, so
   `release` was skipped entirely. A run that read as one broken
   architecture actually shipped zero. fail-fast: false.

And the packaging loop `continue`s past any artifact it does not
recognise, so a missing target would have quietly produced four npm
packages while @hanzo/dev still named five in optionalDependencies —
users on the fifth arch installing a wrapper with no binary under it.
A new step names the five and refuses before packaging.

Also the nine workspace tests that had preflight red, both stale
against the rename to `dev`:

- bash_completion_uses_code_command_name asserted the literal `_code`,
  a second copy of the product name that went stale the moment
  CLI_COMMAND_NAME became "dev". Derive the expectation from the
  constant so it cannot drift again.

- The seven VT100 snapshots recorded a status header that now includes
  `Directory: <cwd>`, resolved from std::env::current_dir() — the
  checkout path of whoever ran the test. Those snapshots only ever
  matched one machine. The harness now pins cwd to /snapshot: absolute
  so it is taken verbatim, outside $HOME so it is never abbreviated to
  ~/…, and outside any git repo so no Branch: segment appears.

Every architecture stays in the matrix. These are native runners
(ubuntu-24.04-arm, macos-14 for both darwin targets) — no QEMU, so
arm64 costs a runner minute, not emulation.

Verified locally on 1.90.0: 8 passed / 0 failed in
vt100_chatwidget_snapshot, 1 passed / 0 failed for the completion test.
2026-08-06 14:23:32 -07:00
zeekay 7c9138a677 sync: just-every/code 00910569e0 — 31 commits, MCP + tool_search + skills
Takes the upstream delta e4fe007150..00910569e0 from just-every/code, our
proximate upstream. hanzo/dev has no merge-base with any upstream (its history
was rewritten, so every SHA differs while the root trees are identical), so
this lands as a 3-way apply of the delta rather than a merge. Zero conflicts:
the upstream delta touches 249 files, our fork diverges in 140, and the two
sets intersect in exactly 2 files (protocol.rs, chatwidget.rs), both pure
additions that leave every Hanzo identity line alone.

Carries the surface the sandbox agent needs, with the codex PR numbers intact:
  57f42a8113 tool search: no clone of immutable metadata on cache hits (#37279)
  e1831db7c3 reuse MCP handlers across sampling steps (#37273)
  bfb6a6ea22 plugin roots in the host skill loader (#37267)
  1151b23f01 start cached MCP servers lazily for subagents (#37261)
  7a0e974e08 harden network proxy MITM authorization (#37211)
  98da2c4499 reserve the `tool_search` namespace for the search tool (#37188)
  1dc93cdeb0 refresh the codex-rs mirror to codex main

codex-rs/ is a vendored mirror that just-every refreshes wholesale; patching it
directly would be overwritten by the next refresh, so we take their refresh
instead of cherry-picking from openai/codex.
2026-08-06 14:17:15 -07:00
zeekayandHanzo Dev 0a1b157d2c feat(exec,app-server): a real JSON contract, and steering that does not abort
Three things the last pass left flagged. Two are done; the third turned out to
belong somewhere else and is reported rather than forced in.

## exec --json emits the schema instead of the SDK guessing it

`exec_events.rs` and `experimental_event_processor_with_json_output.rs` had no
`mod` declaration, so 683 lines defining the `thread.*`/`item.*` schema never
compiled. `sdk/typescript/src/events.ts` says "based on event types from
code-rs/exec/src/exec_events.rs" and then rebuilt those events client-side from
raw core events — the contract existed only in TypeScript, and nothing checked
the two halves agreed.

Declared the modules and fixed what drifted while dead: TaskStarted became a
unit variant, WebSearchEnd became WebSearchComplete, StreamError folded into
Error, ExecCommandEndEvent lost `aggregated_output`, print_config_summary lost a
parameter. `--json` now uses it (`--experimental-json` is an alias, matching
upstream), and the old raw-event processor is deleted rather than left as a
second way to do this. thread.started needed a route because exec consumes
SessionConfigured before the event loop, so EventProcessor grew a defaulted
`on_session_configured`.

Making it real immediately found a lie: the SDK's `Usage` declares five fields
and we emitted three. core::TokenUsage had `cache_write_input_tokens` and
`reasoning_output_tokens` all along; the processor dropped them. Against a mock
model:

  {"type":"thread.started","thread_id":"2bd35c77-..."}
  {"type":"turn.started"}
  {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hello from the model."}}
  {"type":"turn.completed","usage":{"input_tokens":11,"cached_input_tokens":2,
   "cache_write_input_tokens":0,"output_tokens":5,"reasoning_output_tokens":1}}

The SDK now passes those through instead of reconstructing them. Its test suite
did not compile — at HEAD and in just-every upstream: runStreamed.test.ts uses
`path` and `createMockClient` without importing either, testCodex.ts passes
`config` to a type declaring `configOverrides`, and findCodexPath() referenced
four names (`moduleRequire`, `CODEX_NPM_NAME`, `createRequire`,
`PLATFORM_PACKAGE_BY_TARGET`) that were never declared anywhere. Declared them
against @hanzo/dev, added `env` to CodexOptions, pointed the harness at
code-rs/target/debug/dev instead of codex-rs/target/debug/codex.

  pnpm exec jest tests/runStreamed.test.ts --testTimeout=120000
  3 passed  (asserts thread.started, turn.started, item.completed, turn.completed
             and item.type === "agent_message" against the real binary)

## queueUserMessage: steer a turn instead of tearing it

`sendUserMessage` submits Op::UserInput, and core/src/codex/streaming.rs:998
calls `sess.abort()` before spawning a new turn — over app-server you could
interrupt a turn, never redirect one. Op::QueueUserInput folds items into the
running task instead, and only the TUI used it. It is now an app-server method.
Separate method, not a flag: interrupting and steering are different acts and a
caller should not pick the destructive one by accident.

  → {"method":"queueUserMessage","id":3,"params":{"conversationId":"999222eb-…",
     "items":[{"type":"text","data":{"text":"steer me"}}]}}
  ← {"id":3,"jsonrpc":"2.0","result":{}}
  ← {"error":{"code":-32600,"message":"conversation not found: 00000000-…"},"id":4}

## zrok: not ours to build

Assessed and deliberately not implemented here. `hanzo link` already composes
ttyd + share::start + publish_terminal into exactly this feature, and
cli/src/commands/share.rs calls itself "the ONE implementation of 'publish a
local port on the fabric'". `dev` has no framable surface anyway — app-server is
raw JSON-RPC — and no IAM identity to reach POST /v1/share/enable. The gap is in
cloud/cmd/boxd, which already execs `dev`. Details in the report.

Also: git.hanzo.ai release migration precondition written into LLM.md (four
call sites, forge serves /v1/ not /api/v1/, publish to both for a cycle before
moving the updater). SDK package was @openai/codex-sdk; now @hanzo/dev-sdk.

  cargo test -p {code-core,code-protocol,code-cli,code-app-server,
                 code-exec,code-mcp-server,code-tui} -- --test-threads=1
  1311 passed; 1 failed

The failure is git_info::test_collect_git_info_with_remote, still environmental:
the new narrower insteadOf rule only covers hanzoai/*, and the test's URL is
github.com/example/repo, which the broad rule still rewrites.

Version 0.2.189 -> 0.2.190.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 01:13:15 -07:00
zeekayandHanzo Dev daf458296f fix(tui): the header said "Every Code"; three upstream tests assumed OpenAI
The TUI header literal came across in the merge as upstream's own product
name. That is the most-looked-at string in the program:

  code-rs/tui/src/chatwidget.rs  "Every Code" -> "Hanzo Dev"

Regenerating snapshots proves the blast radius exactly — 34 snapshot files
changed, and the whole diff is 46 title lines plus 6 lines where the settings
overlay now reads "Model: ZEN5-Coder" instead of "Model: GPT-5.5". Nothing
else moved, which is the point of running them.

Two more prompts the .md sweep could not see because they live in Rust:
DEFAULT_PERSONALITY_HEADER (prepended to BASE_INSTRUCTIONS whenever a
personality is set, and it asserted "based on GPT-5" even for zen5-coder),
and the sample skill's restart hint.

Three tests arrived with the merge asserting upstream's defaults. Each is
given the provider or model it actually means to exercise, rather than having
its assertion weakened:

  app-server get_auth_status  - requires_openai_auth is false for the `hanzo`
                                provider, so the ChatGPT branch is dead by
                                design. Test now names `openai`.
  tui context_mode override   - 272k is the GPT-5 window; zen5-coder has none.
                                Test now names `gpt-5.5`.
  cli bash completion         - asserted `_code()` while CLI_COMMAND_NAME has
                                been "dev" since the rename. This one was red
                                before the merge too. Now asserts our name.

LLM.md's claim that codex-rs changes nothing a user runs was false until this
merge — `dev` used to compile ten crates out of it. Upstream finished the
migration; recorded what is actually left (one models.json via include_str!).

  cargo test -p {code-core,code-tui,code-cli,code-app-server,
                 code-exec,code-mcp-server,code-protocol} -- --test-threads=1
  1310 passed; 1 failed

The one failure is git_info::test_collect_git_info_with_remote, which asserts
a literal https remote. This machine rewrites it:

  [email protected]:.insteadof https://github.com/

  $ GIT_CONFIG_GLOBAL=/dev/null cargo test -p code-core --lib -- \
      git_info::tests::test_collect_git_info_with_remote
  test result: ok. 1 passed; 0 failed

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 00:08:26 -07:00
zeekayandHanzo Dev 23a7fe1109 feat: the product says its own name, and can be driven from outside
Branding. The reported "OpenAI Codex" was not a banner — the TUI banner has
said "Welcome to Code" all along. It was the model describing itself. Every
request carries BASE_INSTRUCTIONS, and `zen5-coder` matches none of the
OpenAI slug prefixes in find_family_for_model, so it lands on
derive_default_model_family -> core/prompt.md, whose first line read "You are
a coding agent running in the Codex CLI ... led by OpenAI". The agent was
told who it was, and it answered honestly.

Fixed the harness identity in all eight prompts (keeping each model's own
identity: "based on GPT-5" stays, the harness around it becomes Hanzo Dev),
the per-turn header, approval and model pickers, the login success page, MCP
tool titles and approval prompts on both client and server sides, seatbelt
policy header, cloud copy, two stale User-Agents, and the CLI hints that
named the wrong binary. Golden JSON fixtures moved with the code they assert.

Deliberately NOT changed, because they are wire and not text: `OPENAI_*` and
`CODEX_*` env vars, the `codex`/`codex-reply` MCP tool names and the
`code-mcp-server` server name (hosts key allowlists off them), `codex.*` OTEL
event names, the `openai` provider id, model slugs, `~/.code` on disk, and
the `https://api.openai.com/auth` JWT claim namespace.

Control. `cli/src/proto.rs` was a complete bidirectional control plane —
Submission JSON lines in, Event JSON lines out, the whole `Op` set including
QueueUserInput — exported from lib.rs with no subcommand to reach it. Wired
it up as `dev proto`. That is the difference between interrupting a turn and
steering one: `app-server`'s sendUserMessage hardcodes Op::UserInput, which
calls sess.abort() and restarts the turn.

  $ printf '%s\n' '{"id":"s1","op":{"type":"interrupt"}}' \
                  '{"id":"s2","op":{"type":"shutdown"}}' | dev proto
  {"id":"","event_seq":0,"msg":{"type":"session_configured",
   "session_id":"4d82071e-...","model":"zen5-coder",...}}
  {"id":"s2","event_seq":1,"msg":{"type":"shutdown_complete"}}

Also: upstream-merge.yml polled openai/codex every 30 minutes. That is the
wrong upstream (codex-rs reaches us through just-every) and a cron cannot
work against a disjoint history anyway — it needs a hand-supplied merge base.
Retargeted to just-every/code and made it manual, pointing at the procedure
in LLM.md. Purged @just-every/code from the release and recovery pipelines,
which would otherwise have published our binaries under their scope.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 23:28:50 -07:00
zeekayandHanzo Dev dc13f1609d fix(merge): restore the identity the merge reverted, and the fields it added
Three regressions the merge introduced, all caught by building and running:

- `core/src/model_provider_info.rs` — upstream added `auth`,
  `experimental_bearer_token` and `websocket_connect_timeout_ms` to
  ModelProviderInfo; the Hanzo provider now sets them (None: our bearer comes
  from HANZO_USER_KEY, not a command, and the websocket default is fine).
  This was the only compile error in 7004 changed files.
- `core/src/config.rs` — the default provider fallback had reverted to
  "openai". Back to HANZO_PROVIDER_ID. This one is the whole product.
- `cli/src/main.rs` — the entire clap identity had reverted: `--help` printed
  "Codex CLI" and `Usage: code`. Restored name/bin_name = "dev", the about
  line, subcommand help, and the doctor/preview/login hints (17 sites).
- `tui/src/updates.rs` — the in-product upgrade hint had reverted to
  `npm install -g @just-every/code@latest`, which would send our users to
  someone else's package.

`core/tests/hanzo_identity.rs` needed one call-site update for upstream's new
`built_in_model_providers(Option<String>)` signature; it then caught nothing
further, which is the point of it.

  cargo test -p code-core --test hanzo_identity
  4 passed; 0 failed

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 23:17:49 -07:00
zeekayandHanzo Dev 6cbbea2d03 merge: just-every/code v0.6.169 (e4fe007150) — keep Hanzo identity
Our history is just-every's with author emails rewritten, so the graphs are
disjoint and git finds no merge base. Supplied one by hand: d1f1f51a41, the
just-every commit matching our Oct-2025 fork point (22 files apart). 9618
upstream commits, 7004 files, +1.81M/-159k, 37 conflicts.

Resolution:
- Upstream code where our side was the Oct-2025 prototype of a feature they
  have since reimplemented 3-5x larger (agent_runs, browser_sessions,
  history_cell/{agent,browser}, the vt100 harness and its snapshots).
- Upstream's config.rs module split (builder/defaults/sources/validation)
  kept; zen5-coder and the provider-follows-model rule re-applied on top.
- Ours where the file IS the identity: README, package.json, postinstall,
  release notes pipeline, integration-zed.
- Hybrid where upstream improved a file we had branded: updates.rs (their
  release-repo migration, our hanzoai/dev), release.yml (their fail-fast npm
  auth check, our package name), chatwidget.rs (their turn handling, our
  HANZO_USER_KEY hint), exec human output (their 237 lines, our banner).

Superseded and dropped deliberately: our resume_command_name() coder-alias
hack — upstream's derive_resume_command_name() reads the invocation basename
generically, which yields "dev" for us.

LLM.md now records the real topology and the sync point; AGENTS.md and
CLAUDE.md are symlinks to it again. Single parent on purpose: adopting
jecode/main as a second parent would re-import 13,742 commits carrying the
upstream author emails a prior rewrite deliberately removed.

Version 0.2.188 -> 0.2.189.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 23:09:12 -07:00
zeekayandHanzo Dev d3d1dfa1d3 dev is ours: the Hanzo Cloud by default, a binary named dev, our namespaces
hanzoai/dev went public defaulting to someone else's cloud. Four things were
wrong, and all four now have a test that fails if they come back.

DEFAULT PROVIDER AND MODEL. built_in_model_providers() gains `hanzo` -- the
Hanzo Cloud at api.hanzo.ai/v1, Responses wire, bearer from HANZO_USER_KEY --
and it is the default. The default model is zen5-coder. This was not cosmetic:
the old default is not in the gateway catalog, so it 400s against our own
cloud. Measured, not assumed:

  POST api.hanzo.ai/v1/responses model=gpt-5-codex -> 400 "model is not
      available. Use GET /v1/models to list available models"
  POST api.hanzo.ai/v1/responses model=zen5-coder  -> 200, clean output

Our own CLI already documented the bug from the other side -- dev.rs says
"Name the model too -- codex's built-in default is not in the gateway catalog
and would 400" and passes `-c model=` on every launch to work around it. It
also asserts that on the native gateway NO model_provider override is needed,
which is only true once `hanzo` is the built-in default. It is now.

zen5-coder over enso because dev is a coding agent and because enso returns
system-prompt text on the Responses wire that dev actually speaks; zen5-coder
returns the answer. Model ids were read from GET /v1/models with a bearer, not
guessed. Other providers stay fully configurable -- this is the default, not
the only choice.

THE LOGIN COMMAND. Messages said `code login` / `codex login`. The real command
is `hanzo auth login` (`hanzo login` is not a command -- it is swallowed as an
AI task). It now lives once, as auth::LOGIN_COMMAND, so no message can drift
into naming a command that does not exist. References to this binary's own
subcommand became `dev login`.

A BINARY NAMED dev. HEAD built `code` and `codex` and nothing named `dev`, so a
release cut today would ship no `dev` -- and `hanzo code` launches a bare `dev`
off PATH, so every new install would break. `dev` is now the canonical bin,
declared first; `code` remains as an alias to the same program, never the
reverse. The CLI identifies as `dev` / "Hanzo Dev", not "OpenAI Codex".

OUR NAMESPACES. The pipeline published to @just-every/* on npm and pushed
formulae to just-every/homebrew-tap. Both of ours already exist and are already
populated at v0.6.91 -- @hanzo/dev plus all five @hanzo/dev-<target> packages,
and hanzoai/homebrew-tap's Formula/hanzo-dev.rb, which installs `dev` and
expects assets named dev-<triple>. Nothing here is squatted; this is our own
distribution, reclaimed. Assets are renamed to match the formula.

Two live supply-chain bugs fell out of that, both worse than a naming problem:
postinstall.js (runs on every npm install) and the tui auto-updater fetched
from a third party's releases -- the updater also told our users to
`npm install -g @just-every/code@latest`. Worse, a shadowed `isWSL` const put
the two fast paths in the temporal dead zone, so their catch blocks swallowed
the error and EVERY install fell through to that download: the five published
platform packages were never used, and the third-party fetch was not an edge
case but the only path that ran. Fixed and exercised in a sandbox across all
three paths. codex-cli/.pack/ -- a committed `npm pack` artifact CI
regenerates, and the reason the bad URL had two homes -- is now untracked.

DURABILITY, because upstream-merge.yml merges openai/codex every 30 minutes.
A one-time edit to a default is a suggestion that survives until the next
merge. Two mechanisms:

  - .github/merge-policy.json prefer_ours_globs listed only codex-rs/** while
    the workspace that SHIPS is code-rs/**, so it protected nothing that
    ships. The identity-bearing code-rs files are now listed.
  - code-rs/core/tests/hanzo_identity.rs asserts all four properties -- the
    provider, a Zen default, a bin named dev, and no third-party publish
    target. Upstream has no such file, so a merge cannot conflict with it or
    carry the guard away with the thing it guards. Policy can be edited;
    a red build cannot be ignored.

The gate is not decorative: it failed on the un-retargeted pipeline
("release.yml still references `@just-every/`") and went green only when the
pipeline was actually fixed.

Verified: cargo build --bin dev clean; hanzo_identity 4/4; code-core lib
332 passed, 1 failed -- git_info::test_collect_git_info_with_remote, which
fails identically on a clean origin/main checkout here because this machine
has a global [email protected]:.insteadof rewrite. Pre-existing and
environmental, not from this change.

  $ dev exec "say OK"        # fresh profile, no config
  Hanzo Dev v0.0.0
  model: zen5-coder
  provider: hanzo

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 14:05:00 -07:00
James Peterandzeekay 11fa2750eb fix(tui/auto-drive): block resume while auto-resolve pending 2026-07-31 20:35:36 -07:00
James Peterandzeekay f9b95602b9 fix(tui/tools): group agent and browser tool cells 2026-07-31 20:35:35 -07:00
James Peter 30a0a065be Merge branch 'merge/code-branch-20251007-113259' into main 2025-10-08 10:27:28 +10:00
James Peter ba3dd4b8fa fix(history): dedupe assistant messages across finalize/restore 2025-10-07 22:46:12 +10:00
James Peter 3bc12aeb03 merge local main into code-branch-20251007-104544 before merge 2025-10-07 21:51:56 +10:00
James Peter aa6aaae817 fix(tui/history): clear suppressed exec cells 2025-10-07 21:29:04 +10:00
James Peter 8d138ed9bd UX: match resume hint to coder alias 2025-10-07 21:12:35 +10:00
James Peter cd1aa8dc96 fix(tui/resume): avoid duplicate assistant cells on replay 2025-10-07 20:50:32 +10:00
James Peter 382236f47b fix(mcp-server): track acp session_set_model rename 2025-10-07 20:02:39 +10:00
James Peter 2a71c32f13 Merge branch 'code-branch-20251006-101336' into main 2025-10-07 19:44:50 +10:00
James Peter c20501342e test(tui): document vt100 harness and drop flaky regressions 2025-10-07 19:24:36 +10:00
James Peter abfaf1161c fix(core/rollout): keep agent events untouched when resuming 2025-10-07 13:28:35 +10:00
James Peter a8937812a7 fix(core/rollout): preserve user attachments when resuming 2025-10-07 13:20:26 +10:00
James Peter 48d345db86 test: reintroduce minimal tests 2025-10-07 13:02:00 +10:00