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
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:
| check | how it's measured | healthy line | unhealthy line + coached fix |
|---|---|---|---|
| version | the compiled-in package version | wbx 0.13.0 | — always present |
| binary + PATH | current_exe() plus a hand-rolled PATH scan for wbx | binary … / on PATH … | NOT on PATH — add the install dir |
| engine | one request to the runtime's well-known endpoint | engine reachable | unreachable + detail + → wbx deploy local stands one up |
| workspace | is_file() for workbook.org · deployment.org · publish.org in the current directory | workspace workbook.org · publish.org | no 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 file — runtime.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:
| command | exit | why |
|---|---|---|
wbx doctor | 0 | the broken engine is doctor's subject — reporting it is success |
wbx library | 3 | this 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
--versionpinning — 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.