kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
226 lines (191 loc) • 14.7 kB
Markdown
# CLI reference — the `kestrel` command
The package ships a `kestrel` binary: a router over a small set of verbs. Every verb
renders to `stdout` as a pure payload channel and reports failures on `stderr` with a
stable machine-parseable `code` and a nonzero exit — agents match on `code`, not prose.
This page consolidates the verbs, their key flags, the runtime each requires, and the
exit-code taxonomy. For each verb's full flag list, run `kestrel <verb> --help`, which
stays on the light runtime (it never loads the heavy runtime just to print usage). For the
programmatic surface, see the [API reference](./api-reference.md).
```bash
kestrel <command> [args] [global flags]
kestrel <command> --help # that one command's usage + its own flags
```
## Light vs heavy verbs (the runtime requirement)
The verbs split on the runtime they need:
- **Light verbs run on Node** (or Bun, or a Worker). They parse, validate, print, render a
Frame, report the version, or print help — no native dependency. These are `parse`,
`validate`, `print`, `frame`, `percept`, `version`, and `help`.
- **Heavy verbs execute on Bun** (and, for the registry, the native store). They run graded
sessions and query the run registry. These are `run`, `day`, `runs`, `lineage`,
`leaderboard`, and `agent`. You do **not** need to install Bun: the package bundles it, so
`npx kestrel.markets run …` works on a Node-only host.
The bundled binary is structured so every light verb loads without the heavy runtime on the
import graph; the heavy runtime is pulled in only when a heavy verb is actually invoked.
### How the runtime is found
The published `kestrel` bin is a Node launcher. A light verb is executed in-process under
whatever runtime you invoked it with. A heavy verb, when you are already on Bun, likewise runs
in-process; under Node the launcher **re-execs** the CLI onto a Bun binary with your argv
unchanged and propagates the child's exit code (a signalled child surfaces as `128 + signo`,
so Ctrl-C is still 130). Grading therefore always runs on Bun by construction.
Every candidate is **probed** (`bun --version` must exit 0 with a version at or above the
`engines.bun` floor) before it is used, so a broken shim on `PATH` — or a Bun older than the
floor, which would otherwise die deep in the registry with a misleading SQLite error — is
skipped rather than exec'd. Bun is looked up in this order:
1. `$KESTREL_BUN` — an explicit path to a Bun binary. This is a **pin**: if it does not probe
clean, the CLI refuses (exit 4). It never falls back to a Bun you did not name.
2. Any `bun` on your `$PATH`, in order.
3. The shared runtime cache: `$KESTREL_HOME/.kestrel/runtime/bin/bun` (else `~/.kestrel/runtime/…`).
The first working Bun the launcher resolves is **materialized** here once — its bytes are copied
(a hardlink when same-filesystem, a real copy on a cross-device home), so every other checkout /
install on the same machine reuses it instead of probing (or installing) its own copy. The cache
holds independent bytes, **never a symlink** into an install's `node_modules`, so it keeps working
after that install is deleted (worktree cleanup) or was skipped (`--omit=optional`) — the exact
cases the shared cache exists to serve. Populating it is best-effort and atomic (a temp file
published by rename), so concurrent installs never see a half-written binary.
4. The bundled `bun` optional dependency.
Only when **none** of these yields a working Bun does a heavy verb fault **loud** with
`RUNTIME_UNAVAILABLE` (exit 4) — never a raw resolution stack.
| Variable | Effect |
| --- | --- |
| `KESTREL_BUN` | absolute path to the Bun binary the launcher must re-exec onto (a pin: no fallback) |
| `KESTREL_HOME` | base dir for the shared `.kestrel/runtime` runtime cache (and the credential store); defaults to `~` |
| `KESTREL_NO_BUNDLED_BUN=1` | ignore the bundled dependency (use only `$KESTREL_BUN` / `PATH` / the shared runtime cache) |
**Install weight.** The bundled runtime is not small, and the cost is platform-dependent — npm
keeps every platform variant it downloaded, and only Bun's postinstall picks the right one.
Measured (`bun` + `@oven` on disk):
| Host | Installed |
| --- | --- |
| macOS arm64 | ≈ 61 MB |
| Linux arm64 (glibc) | ≈ 172 MB |
| Linux arm64 (musl / Alpine) | ≈ 259 MB |
| Linux x64 | ≈ 347 MB (npm retains all four x64 variants) |
The published tarball stays ~2 MB; this is install weight, not download weight.
`npm i kestrel.markets --omit=optional` skips the runtime in a **project** install: light verbs
are unaffected, and heavy verbs then require a Bun on `PATH`, in `KESTREL_BUN`, or in the shared
`~/.kestrel/runtime` cache (and refuse with exit 4 if none exists). On a shared machine this is the
recommended layout — install once with the runtime, `--omit=optional` everywhere else, and every
checkout reuses the one cached Bun. npm ignores `--omit=optional` for **global** (`-g`) installs and fetches
the runtime regardless — there, set `KESTREL_NO_BUNDLED_BUN=1` to ignore the bundled copy at run
time.
## Verbs
| Verb | Runtime | Purpose | Key flags |
| --- | --- | --- | --- |
| `version` | Node | print the version and exit | — |
| `help [command]` | Node | print usage, or one command's usage | — |
| `parse <file>` | Node | parse + validate a plan document | `--arm`, `--bus`, `--instruments` |
| `validate <file>` | Node | parse + validate (alias of `parse`) | `--arm`, `--bus`, `--instruments` |
| `print <file>` | Node | canonical re-print of a plan document | — |
| `frame <input.json>` | Node | render a Frame from a fixture/snapshot JSON | `--input`, `--kind`, `--view`, `--seat`, `--seat-views` |
| `percept <input.json>` | Node | render a Frame (alias of `frame`) | `--input`, `--kind`, `--view`, `--seat`, `--seat-views` |
| `run` | Bun | grade a session against a recorded bus, auto-record | `--bus`, `--plans`, `--fill`, `--r-usd` |
| `day` | Bun | stepped / wake session (wake handshake + supersession), auto-record | `--bus`, `--dir`, `--fill`, `--r-usd` |
| `runs list` | Bun | query recorded runs | `--session-date`, `--fill`, `--lineage`, `--db` |
| `runs show <run_id>` | Bun | show one recorded run + its plans | `--db` |
| `lineage <name>` | Bun | show a plan lineage | `--db` |
| `leaderboard` | Bun | ranked leaderboard over recorded runs | `--since`, `--mode`, `--db` |
| `agent` | Bun | machine mode: a JSONL request/response stream over the SDK | `--api` (transport) |
There is **no `grade` verb.** Grading is not a standalone step: it happens inside `run` and
`day`, which grade the session as they run it and record the result.
### Light verbs
- **`version`** — prints `kestrel <version>`; in `--json`, `{ schema, version }`. Exit 0.
- **`help [command]`** — with no command word, prints the top-level usage (commands, global
flags, and the exit-code taxonomy). With a command word (`kestrel <cmd> --help` or
`kestrel help <cmd>`), prints that command's own usage and flags. Exit 0.
- **`parse` / `validate <file>`** — parse and validate a plan document. A parse failure
exits in the `USAGE` bucket (exit 2) carrying the `PARSE` code from the language layer.
Parse-green is **not** arm-green: `--arm` (or `--bus <tape>` / `--instruments <SYM[,SYM…]>`,
which imply it) additionally runs the engine's arm tier — the same `armDocument` a real `run`
raises, so the refusal shown is byte-identical. `--bus` derives the session instruments from
the tape's META exactly as `run` resolves them; `--instruments` names them directly, validated
as option-underliers; bare `--arm` with no context source refuses fail-closed. An arm-tier
refusal also exits in the `USAGE` bucket (exit 2), with the distinct codes `ARM` (the document
refuses to arm), `ARM_CONTEXT` (no derivable session context), or `BUS` (the `--bus` tape is
unusable).
- **`print <file>`** — the canonical re-print; `print(parse(text))` is byte-stable.
- **`frame` / `percept [<input.json>|-]`** — render a Frame from a fixture/snapshot JSON,
read from a path argument, `--input <p>`, or `-` / stdin. `--kind <briefing|wake>`
overrides the inference (the presence of a wake index implies a wake Frame).
**View addressing** (local backend): `--view <name[:panes]>` selects the View the Frame
renders under — a bare `name` resolves against the shipped named-View registry (the
founder Views, e.g. `strategist-open`, `watcher-wake`, `scan-fire`), while
`name:pane[ arg…][,pane…]` defines an inline View in the language's own pane syntax
(e.g. `--view "custom:delta,tape 5m"`). `--seat <pm|strategist|watcher>` binds the
acting pod seat, so a seat with a founder seed for the frame kind reads its founder
View; `--seat-views founder` is the explicit opt-in assertion of that same behaviour
(requires `--seat`). Resolution precedence is strict: explicit `--view` > seat founder
View > phase default. **Fail-closed:** an unknown View name, unknown seat, malformed
pane/arg token, or a pane the catalog refuses is a loud typed `USAGE` error (exit 2) —
never a silent fall-through to the phase-default panes. The remote backend has no
percept route and fails closed as before.
### Heavy verbs
- **`run`** — grade a plan document against a recorded market bus, print the report, and
auto-record the run. Required: `--bus <p>` (the recorded bus), `--plans <p>` (the plan
document — a `.kestrel` file, or a Markdown file with fenced kestrel blocks),
`--fill <m>` (`strict-cross-v1` or `maker-fair-v1`), and `--r-usd <n>` (the dollar value
of 1R, positive). Optional: `--tau-hours`, `--hazard-params` (maker-fair only), `--out`
(also write the report JSON), `--db` (registry path), `--no-record` (grade only).
- **`day`** — run a stepped / wake session with a wake handshake and document supersession,
and auto-record it. Required: `--bus`, `--dir <d>` (the handshake directory the session
authors plans into), `--fill`, `--r-usd`. **The default terminates:** with no author wait
declared, `day` runs from whatever handshake documents are staged in `--dir`, and a missing
document refuses promptly with `NO_AUTHOR`/exit 6, naming the file it wanted — it never rides
a 900s poll for a reply the caller never declared. Optional: `--wakes <HH:MM,…>` (explicit wake
clock times, ET), `--await-author` (declare an out-of-band author WILL reply: block at each
handshake up to `--max-wait`, then abandon loudly with `HANDSHAKE_ABANDONED`/exit 6),
`--max-wait <s>` (seconds to wait for each handshake reply under `--await-author`, default 900;
supplying it implies `--await-author`), `--no-author` (the explicit form of the default:
documents are pre-staged in `--dir`, a miss is the same prompt `NO_AUTHOR`/exit 6 refusal),
`--structural` (structural-cadence wakes), plus the same `--tau-hours` / `--hazard-params` /
`--out` / `--db` / `--no-record` as `run`. `--await-author` and `--no-author` together are a
usage error (contradictory declarations). Every handshake block is announced on stderr before
it is entered and heartbeats while it waits, so a blocked day is never mistaken for a hung one.
- **`runs list`** — query recorded runs, optionally filtered by `--session-date`, `--fill`,
or `--lineage`; `--db <p>` selects the registry path.
- **`runs show <run_id>`** — show one recorded run and its plans; `<run_id>` may be a 12-hex
prefix. A missing run exits `NOT_FOUND` (exit 3).
- **`lineage <name>`** — show a plan lineage (the graded runs sharing a plan name). A
missing lineage exits `NOT_FOUND` (exit 3).
- **`leaderboard`** — the ranked leaderboard over recorded runs; `--since <ms>` and
`--mode <m>` narrow it.
- **`agent`** — the machine/agent mode: reads a JSONL request stream on stdin (one verb
envelope per line) and writes versioned protocol JSONL on stdout, forwarding each verb to
the SDK. It is a thin projection of the SDK surface (catalog / validate / the Session
lifecycle / grade / artifact / operation continuation). `--api` selects the transport
only: bare or absent uses the local transport; `--api <url>` (or `KESTREL_API`) uses the
remote transport.
## Global flags
These apply to every verb (they are peeled before the command runs):
| Flag | Effect |
| --- | --- |
| `--json` | machine JSON output, even on a TTY |
| `--format <json\|text\|human>` | force a render mode |
| `--agent` | agent mode: text, no color, non-interactive |
| `--color <always\|never\|auto>` | colorize (human mode only) |
| `--no-color` | disable color (`NO_COLOR`-equivalent) |
| `-h`, `--help` | print usage and exit 0 (per command: `kestrel <cmd> --help`) |
| `-V`, `--version` | print version and exit 0 |
`stdout` stays a pure payload channel: an error is written to `stderr` in the active mode,
so an agent's `JSON.parse(stdout)` never sees a half-written object followed by an error.
## Exit-code taxonomy
Exit `0` iff the command truly succeeded; every error path is nonzero. The `code` strings
are stable across releases — match on `code`, not on the message.
| Code | Exit | When |
| --- | --- | --- |
| `OK` | 0 | the command succeeded |
| `GENERIC` | 1 | an unexpected / uncaught error (`KESTREL_DEBUG=1` dumps the stack) |
| `USAGE` | 2 | bad/unknown flag, missing required flag, bad `--format`, a parse failure (code `PARSE`), or a `parse`/`validate --arm` refusal (codes `ARM` / `ARM_CONTEXT` / `BUS`) |
| `NOT_FOUND` | 3 | `runs show <id>` / `lineage <name>` / an input file is absent |
| `RUNTIME_UNAVAILABLE` | 4 | no working Bun could be found (or the native store failed to load) |
| `PAYMENT_REQUIRED` | 5 | a remote verb returned a 402/Offer (surfaced as data) |
| `SIGINT` | 130 | interrupted (Ctrl-C) |
A parse failure is a `USAGE` exit (2) carrying the more specific `PARSE` code raised by the
language layer; a `parse`/`validate --arm` refusal shares that exit bucket with its own specific
codes (`ARM`, `ARM_CONTEXT`, `BUS`). `RUNTIME_UNAVAILABLE` is reserved for a host where the launcher found **no**
working Bun in any tier — `$KESTREL_BUN`, `PATH`, or the bundled dependency; it fails closed
with a hint naming those escape hatches, never a raw stack. A heavy verb re-exec'd onto Bun
returns the child's own exit code, and a signalled child surfaces as `128 + signo`.
## See also
- **[API reference](./api-reference.md)** — the eight published entry points and their types.
- **[Capability truth](./capability-truth.md)** / **[status page](./status.md)** — what each
verb's runtime actually does today, graded honestly.
- **[Overview](./overview.md)** — what Kestrel is and the four statement kinds.