learn / 07·3 — under wbx · doctor

a report,NOTa judge

wbx doctor reports your environment and engine health — and then refuses to fail you for it. It always exits 0; a dead engine is a fact about your setup, not a fault in the tool reporting it. The same function is bare wbx, so typing nothing gets you a health check plus your next three moves — and agents get the whole picture as one JSON object.

the checkup10 min read
A small technician standing before a colossal glowing diagnostic console that calmly reads out the state of a vast machine on amber dials and gauges — no red alarms, just steady lit readings — 1970s sci-fi style, bright and monumental

debug the tool before the TOOL

Every ecosystem charges the same tax up front. Before you can use the thing, you have to prove the thing is installed right — is the binary on your PATH, is anything listening, why does this command work on your laptop and fail in CI. You debug the tool before you get to use the tool.

The classic answer is a doctor command — brew doctor, flutter doctor — and they mean well. But they judge you: a wall of red ✗ marks and, worse, a nonzero exit code. That exit code is a small disaster. It breaks the script that ran the check, and it panics an agent that reads any nonzero status as "stop, something is wrong." A diagnostic that fails the caller for diagnosing is a diagnostic you can't put in a pipeline.

An ecosystem whose whole posture is the tool does the work can't ship that. If the everyday verbs are supposed to look after you, the verb that checks your setup has to be the gentlest one of all.

the DEFINITION

doc·tor /ˈdɒk·tər/ command

1. environment + engine health, reported, not judged — four real probes classified into a report that always exits 0, rendered two ways: prose for you, one JSON object for an agent.

It belongs to the everyday family — the handful of verbs where the tool looks after itself and you. One sentence each: doctor diagnoses without failing; status and bare wbx are the same checkup wearing a welcome mat; open launches a file in your OS opener; upgrade self-updates and first checks who installed it; completions prints shell completions from the same definition as --help. This lesson is the whole family, anchored on the one that sets its manners.

the four CHECKS

Doctor runs exactly four probes, all real measurements of your machine — no scoring, no grades. Each one resolves to a fact and a coaching line if the fact is unwelcome:

checkhow it's measuredhealthy lineunhealthy line + coached fix
versionthe compiled-in package versionwbx 0.13.0— always present
binary + PATHcurrent_exe() plus a hand-rolled PATH scan for wbxbinary … / on PATH …NOT on PATH — add the install dir
engineone request to the runtime's well-known endpointengine reachableunreachable + detail + → wbx deploy local stands one up
workspaceis_file() for workbook.org · deployment.org · publish.org in the current directoryworkspace workbook.org · publish.orgno org files here (start: wbx init <name>)

Read that table as four independent questions, none of which can sink the ship. Version is the binary telling you which binary it is. Binary-plus-PATH reports two lines — where this executable lives, and what a PATH scan finds for the name — and here's an honest nuance: it shows you both and lets you read them; it does not compare them and render a "mismatch" verdict. The engine probe is one network call, classified three ways (reachable, auth-rejected, unreachable) and never thrown. Workspace just asks whether the three org files that mark a project live in this folder.

Every unhealthy line carries its own fix inline — not a generic "see docs," but the literal next command. That is the family's signature: never a bare error where a default plus a note would do.

what "engine" actually MEANS

depth rung · skippable — exactly where doctor looks for the engine

The engine probe is the only check that reaches off your machine, so it's worth knowing precisely what it reads — because then you can fix what it reports. Doctor doesn't invent its own discovery; it asks the same question every engine verb asks, and the resolution runs in a fixed order:

flowchart TD
  start["wbx doctor — resolve the engine"]
  env{"WBX_ENGINE_URL
or WB_ENGINE_URL set?"} envyes["use that URL
+ optional _TOKEN"] disco{"runtime.json present?
sh.workbooks/disco/runtime.json"} discoyes["127.0.0.1:<port>
with bearer token"] none["no discovery file"] probe["GET /.well-known/workbooks-runtime"] reach["reachable
body, truncated to 120 chars"] auth["auth-rejected
error says unauthorized / 401"] un["unreachable
error, truncated to 160 chars"] start --> env env -- yes --> envyes --> probe env -- no --> disco disco -- yes --> discoyes --> probe disco -- no --> none --> un probe --> reach probe --> auth probe --> un style start fill:#ffffff,stroke:#121316 style envyes fill:#d9dbd3,stroke:#121316 style discoyes fill:#d9dbd3,stroke:#121316 style probe fill:#a8d4f0,stroke:#121316,stroke-width:2px style reach fill:#13d943,stroke:#121316,stroke-width:2px style auth fill:#f2ddb0,stroke:#121316 style un fill:#f3c5a3,stroke:#121316

Walk the chain top to bottom. First, an environment override wins: set WBX_ENGINE_URL (or its WB_ fallback, and an optional token alongside) and that's the engine, full stop. Otherwise doctor looks for a discovery fileruntime.json in the per-OS app directory (~/Library/Application Support/sh.workbooks on macOS, ~/.local/share/sh.workbooks elsewhere), holding a scheme, a port, and a token, which point the call at 127.0.0.1 with a bearer token. If neither exists, the call has nowhere to go, and that missing-file error — no runtime discovery at <path> — is the runtime running? (try wbx deploy local) — gets swallowed into engine: unreachable with the detail attached. Whatever the call returns, the result is classified into one of three states, never raised as an exception.

One naming wrinkle, told honestly: the docs say WBX_*, but every variable accepts WBX_ first and the bare WB_ form as a fallback — both work. The human report's engine line even labels the override (WB_ENGINE_URL=…) when one is set, despite WBX_ also being accepted. Cosmetic, but worth knowing so the label doesn't confuse you.

bare wbx is doctor in a WELCOME mat

Here's where the report-not-judge posture pays its biggest dividend. Bare wbx with no subcommand — and wbx status, the same function — isn't a usage dump. It's the where-am-I landing: doctor's whole report, plus orientation.

You get the prose health check followed by a four-line footer of next moves — start here, the engine, the parts, everything. Type nothing, learn where you stand and what to do next. An agent gets doctor's JSON with one extra key merged in: verbs, the entire verb tree. One call, and a cold agent is oriented — it knows the tool's health and every verb, sub-verb, arg, and flag, in a single round trip.

sequenceDiagram
  participant A as a cold agent
  participant W as wbx
  participant E as the engine
  A->>W: wbx --json
  Note over W: piped stdout → agent mode, automatically
  W->>E: GET /.well-known/workbooks-runtime
  E-->>W: reachable (or not — classified either way)
  W-->>A: {ok, data:{version, engine, workspace, verbs:{…full tree…}}}
  Note over A: one round trip — health + the whole verb tree
  A->>W: now calls a real verb, no guessing at flags
  

Read that exchange as the orientation it is. The agent runs bare wbx --json; because stdout is piped, the tool flips to its structured form on its own — no flag required to be machine-friendly when no human is watching. wbx probes the engine, classifies whatever comes back, and answers with one object that bundles the health report and the full verb tree together. The agent reads it once and now knows both the state of the world and every move available in it — so its next call is a real verb with real flags, not a guess. There's a bare-tree form too: wbx help --json returns just that tree, intercepted before the argument parser even runs. And because the tree is generated from one definition, that intercept, the landing's verbs key, and --help can never disagree.

exit ZERO, by contrast

Now the contract, stated as sharply as the code states it. Run the doctor against a dead engine and ask the shell what happened. Then run a verb that actually needs the engine, on the very same dead engine:

$ wbx doctor
wbx 0.13.0
binary     /Users/ada/.local/bin/wbx
on PATH    /Users/ada/.local/bin/wbx
engine     unreachable
           no runtime discovery at …/sh.workbooks/disco/runtime.json — is the runtime running? (try `wbx deploy local`)
           → `wbx deploy local` stands one up
workspace  no org files here (start: `wbx init <name>`)
$ echo $?
0                       # reported, not judged

$ wbx library; echo $?  # an ENGINE verb on the same dead engine
wbx: …connection refused…
     no engine reachable — start one with `wbx deploy local` or set WB_ENGINE_URL
3                       # the verb that NEEDED the engine carries the code

Same dead engine, two different exit codes — and the difference is the whole idea:

commandexitwhy
wbx doctor0the broken engine is doctor's subject — reporting it is success
wbx library3this verb needed the engine to do its job — its job genuinely failed

That's the line the design draws. A diagnostic's job is to describe the state of the world, so any state it can describe is a job done — exit 0, every time, in every health state. A working verb's job is to act, so a state that blocks the action is a real failure that earns a real code. Code 3 is EXIT_ENGINE, it's marked retryable, and its hint literally names the command that fixes it. The full map — 0 ok, 2 usage, 3 engine, 4 not found, 5 verify, 6 conflict, 7 auth, 1 other — and the one-envelope contract belong to the modes deep dive; this page only needs the contrast. One honest caveat: a genuine usage mistake like wbx doctor --bogus still exits 2, because the argument parser owns that code before doctor ever runs.

the tree that can't DRIFT

depth rung · skippable — why completions are always correct

wbx completions <shell> prints shell-completion scripts for bash, zsh, fish, elvish, and powershell straight to stdout. The detail that matters: those completions are generated from the same command definition as --help and wbx help --json. One source of truth, three renderings. A verb can't appear in help and go missing from completions, because there's no second list to fall out of sync — they're the same tree, printed differently.

To wire them up, source the output the way your shell expects. Conventionally:

echo 'eval "$(wbx completions zsh)"' >> ~/.zshrc      # zsh
wbx completions bash > ~/.local/share/bash-completion/completions/wbx
wbx completions fish > ~/.config/fish/completions/wbx.fish

Those one-liners are the standard recipes for each shell, offered as convention — wbx prints the script; where you source it from is your shell's business, not something the tool prescribes.

self-update that knows who INSTALLED it

wbx upgrade updates the binary in place — but its first move is to check how the binary got there, because the right way to upgrade depends on who owns the install. It's a three-way branch:

flowchart TD
  up["wbx upgrade"]
  npm{"running from
under node_modules?"} npmout["installed via npm — upgrade with:
npm update -g @work.books/cli
(returns Ok — a note, not an error)"] fetch["GET github releases
first tag matching wbx-v*"] same{"latest == current?"} current["already current: wbx 0.13.0"] dl["download wbx-{os}-{arch}
→ wbx.new → chmod 755"] swap["atomic rename over the live binary
upgraded: wbx 0.12.0 → 0.13.0"] up --> npm npm -- yes --> npmout npm -- no --> fetch --> same same -- yes --> current same -- no --> dl --> swap style up fill:#ffffff,stroke:#121316 style npmout fill:#f2ddb0,stroke:#121316 style current fill:#d9dbd3,stroke:#121316 style swap fill:#13d943,stroke:#121316,stroke-width:2px

Follow the three exits. First branch: if any component of the running executable's path is node_modules, the binary was installed by the npm launcher — so wbx returns Ok (not an error) with the note installed via npm — upgrade with: npm update -g @work.books/cli. Self-swapping under the npm package would desync it, so the right move is a note that hands you back to the package manager that owns the install — never an error where a note will do. Second exit: not under npm, so it fetches GitHub releases of workbooks-sh/workbooks.sh, takes the first tag matching wbx-v* (the repo also carries desktop-v* tags, hence the filter), and if that's your current version it says so and stops. Third exit: you're behind, so it downloads the asset for your OS and arch, writes it to wbx.new, marks it executable, and atomically renames it over the running binary.

the small VERBS

depth rung · skippable — the rest of the everyday family

wbx open <file> rounds out the family. It bails if the path doesn't exist, canonicalizes it, and hands it to your OS opener — open on macOS, cmd /C start on Windows, xdg-open on Linux — then checks the opener's exit status so a failure to launch is a real failure, not a silent shrug.

None of these verbs are decoration. The spec's history records why they exist: doctor and completions were priority one — "the first ten minutes," launch-blocking, the verbs you reach for before you've done anything else. open, status, and upgrade were priority two — daily ergonomics. They're all shipped now, and they share a single directive the spec names outright: human mode is dumb simple, the tool does the work, the learning curve is a bug. Bare wbx orients you; upgrade detects npm and redirects; doctor diagnoses without failing; every success teaches the next verb. The family is that sentence, implemented.

what doctor WON'T do

The honesty section. The everyday family is small on purpose, and its limits are real:

  • Doctor reports, it doesn't fix. There is no --fix. It tells you what's wrong and the command that fixes it; running that command is on you.
  • The PATH-vs-binary lines are shown, not judged. Doctor prints where the executable lives and what a PATH scan finds, but it does not compare them or flag a mismatch. You read the two lines and draw the conclusion.
  • Engine detail is truncated. A reachable body is cut to 120 characters, an unreachable error to 160. Enough to orient, not the full payload — for the raw thing, hit the endpoint directly.
  • Upgrade trusts the transport. No checksum or signature verification of the downloaded binary beyond GitHub Releases' TLS, and no --version pinning — it fetches latest only. The in-place swap is unix-shaped; renaming over a running executable is a known sharp edge on Windows, where the file is locked, and the code carries no special case for it.
  • The wasm build bails honestly. Compile wbx to WebAssembly and upgrade, open, and the PATH scan have nothing to act on — they say needs the native binary rather than pretend. Why a native tool can't fully be itself in the sandbox is the seam.

questions people actually ASK

Why doesn't doctor exit nonzero when the engine is down?

Because the down engine is doctor's subject, not its failure. A diagnostic that reports a broken state did its job — exit 0. Failing the caller for diagnosing breaks scripts and panics agents, which is the exact problem doctor exists to avoid. The verb that genuinely needed the engine — like wbx library — is the one that exits 3.

Doctor says unreachable — now what?

Two paths, both in the report. Stand one up locally with wbx deploy local (doctor's prescribed fix, printed inline), or point at an existing engine by setting WBX_ENGINE_URL (its WB_ form works too). Re-run doctor and the engine line flips to reachable.

I installed via npm and upgrade refuses — bug?

No — that's the design. When the binary runs from under node_modules, the npm launcher owns the install, and self-swapping under it would desync the package. So upgrade returns cleanly with the right command: npm update -g @work.books/cli. A note, not an error.

Where does doctor look for the engine?

In order: the WBX_ENGINE_URL / WB_ENGINE_URL env override first; otherwise the discovery file runtime.json in sh.workbooks/disco/ under your OS app dir, which points the call at 127.0.0.1 with a bearer token. No file and no override means unreachable.

Can agents rely on the JSON field names?

Yes — that's their whole point. The agent form is a stable object: version, binary, on_path, engine (with state, url, detail), and workspace. Under --json it's wrapped in the standard {ok, verb, data} envelope, and bare wbx --json adds the full verbs tree. Branch on the fields, not on scraped prose.

Is status different from bare wbx?

No — wbx status is the same function as typing wbx with no subcommand: the doctor report plus orientation. It exists so the landing has a name you can write down in a runbook.

keep GOING

The checkup is the door into the rest of the command — and the engine it keeps asking about.