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.
719 lines (703 loc) • 48.6 kB
text/typescript
/**
* # cli/commands/meta — `version` + `help` (LIGHT, dependency-free)
*
* Zero dynamic imports. The version string comes from a top-level JSON import of `package.json`
* (a compile-time constant once bundled — no runtime dependency, no `.ts` on the graph). These
* handlers are fast-pathed in {@link ../index.ts} before the router ever touches a heavy handler.
*
* `help` is a DISPATCHER (kestrel-1qc): with no command word it renders the top-level usage, but
* `kestrel <cmd> --help` (or `kestrel help <cmd>`) renders that ONE command's own usage + its own
* flags. The per-command help tables ({@link SUBHELP}) are static data authored here — they carry
* NO import of the heavy command modules, so `<cmd> --help` stays LIGHT (node-runnable, no bun/chdb)
* exactly like the top-level short-circuit. The exit-code taxonomy ({@link EXIT_CODES}) mirrors
* `src/cli/errors.ts` `EXIT` and is surfaced in the top-level `help` so agents can discover it.
*/
import type { OutputCtx } from "../context.ts";
import pkg from "../../../package.json" with { type: "json" };
// The signing-input prefix is a WIRE constant homed in the dependency-free protocol package
// (OSS-ADR-0046 §2, kestrel-s67u); interpolated into help text so the docs can't drift from the
// prefix the verifier actually rebuilds the signed message with.
import { GRADE_SIGN_PREFIX } from "../../protocol/attestation.ts";
/** The published version, injected at bundle time from `package.json`. */
export const VERSION: string = pkg.version;
/** The command inventory, rendered by `help`. */
const COMMANDS: readonly { name: string; args: string; purpose: string }[] = [
{ name: "version", args: "", purpose: "print the version and exit" },
{ name: "help", args: "[command]", purpose: "print this usage (or one command's usage) and exit" },
{ name: "parse", args: "<file>", purpose: "parse + validate a plan document" },
{ name: "validate", args: "<file>", purpose: "parse + validate a plan document (alias of parse)" },
{ name: "print", args: "<file>", purpose: "canonical re-print of a plan document" },
{ name: "card", args: "[<topic>]", purpose: "print the shipped agent language card (offline); a topic prints a walkthrough (see `kestrel card --help`)" },
{ name: "frame", args: "<input.json>", purpose: "render a Frame from a fixture/snapshot JSON" },
{ name: "percept", args: "<input.json>", purpose: "render a Frame (alias of frame)" },
{ name: "register", args: "[--name <n>] [--scopes <s,s>] [--no-git-identity]", purpose: "self-register as an autonomous agent; store the credential" },
{ name: "whoami", args: "", purpose: "inspect the stored credential: identity, scopes, expiry + status, API host, file path (no network)" },
{ name: "refresh", args: "", purpose: "renew the stored durable capability before it expires (server-minted; no client RNG)" },
{ name: "secrets", args: "set <KEY> [--stdin] | list | unset <KEY> | path", purpose: "manage operator/BYOK secrets in ~/.kestrel/.env (value via prompt or --stdin, NEVER argv; list is names-only)" },
{ name: "sim", args: "[<scenario-slug>] [--plans <p>] [--budget <amount>]", purpose: "run a curated hosted scenario, free (bare: the menu); --budget opts into the paid boundary" },
{ name: "prove", args: "[<scenario-slug>] [--plans <p>] [--copy-token <t>]", purpose: "the zero-credential front door: bare runs a default scenario + a proof URL, no key/config/prompt" },
{ name: "replay", args: "<proofId>", purpose: "reproduce a local proof (fresh trial), or verify a published one" },
{ name: "verify", args: "<url|proofId>", purpose: "zero-trust re-verify of a published proof's signature (never trusts the body's own verdict)" },
{ name: "certify", args: "<url|proofId>", purpose: "open recomputation: re-project the Blotter LOCALLY and reproduce the hosted result byte-identically (gate G10)" },
{ name: "agent", args: "[--api <url>]", purpose: "machine mode: a JSONL request/response protocol over stdin/stdout (see `kestrel agent --help`)" },
{ name: "mcp", args: "[--local]", purpose: "serve the MCP face over stdio (JSON-RPC 2.0) for an MCP client; remote by default, --local for the in-process engine (see `kestrel mcp --help`)" },
{ name: "run", args: "--bus <p> --plans <p> --fill <m> --r-usd <n>", purpose: "grade a session, auto-record" },
{ name: "day", args: "--bus <p> --dir <d> --fill <m> --r-usd <n>", purpose: "stepped/wake session, auto-record" },
{
name: "paper",
args: "--instrument <s> --plans <p> --session-date <d> --r-usd <n> --max-order-qty <n> --max-position-qty <n> --max-notional-usd <n>",
purpose: "run a PAPER session on a live IB Gateway feed (simulated fills, never live money)",
},
{ name: "runs list", args: "[--session-date <d>] [--fill <m>] [--lineage <n>]", purpose: "query recorded runs (hosted rows show the plan/strategy each used)" },
{ name: "runs show", args: "<id>", purpose: "show one run: a local run + plans, or a hosted `sim` receipt + proof URL" },
{ name: "runs compare", args: "<runA> <runB>", purpose: "diff two hosted `sim` receipts — which plan each used + the orders/fills/P&L deltas (\"did my tweak help?\")" },
{ name: "lineage", args: "<name>", purpose: "show a plan lineage" },
{ name: "leaderboard", args: "[--since <ms>] [--mode <m>]", purpose: "ranked leaderboard" },
];
const GLOBALS: readonly { flag: string; purpose: string }[] = [
{ flag: "--json", purpose: "machine JSON output (even on a TTY)" },
{ flag: "--api <url>", purpose: "select the managed remote transport (`--api default` ⇒ api.kestrel.markets; also KESTREL_API); default is local" },
{ flag: "--format <json|text|human>", purpose: "force a render mode" },
{ flag: "--agent", purpose: "agent mode: text + no color + non-interactive" },
{ flag: "--color <always|never|auto>", purpose: "colorize (human mode only)" },
{ flag: "--no-color", purpose: "disable color (NO_COLOR-equivalent)" },
{ flag: "-h, --help", purpose: "print usage and exit 0 (per-command: `kestrel <cmd> --help`)" },
{ flag: "-V, --version", purpose: "print version and exit 0" },
];
/**
* The exit-code taxonomy — a VERBATIM mirror of the `EXIT` map in `src/cli/errors.ts`. Every error
* path is nonzero; `code` strings are stable across releases (agents match on `code`, not prose).
* Surfaced in the top-level `help` so the taxonomy is discoverable from the CLI itself. NOTE: a
* parse failure carries the `code: "PARSE"` (raised by the lang layer) inside the USAGE exit bucket.
*/
const EXIT_CODES: readonly { code: string; exit: number; when: string }[] = [
{ code: "OK", exit: 0, when: "the command succeeded" },
{ code: "GENERIC", exit: 1, when: "an unexpected/uncaught error (KESTREL_DEBUG=1 dumps the stack)" },
{ code: "USAGE", exit: 2, when: "bad/unknown flag, missing required flag, bad --format, or a parse/arm failure (code PARSE, or — under validate/parse --arm/--bus/--instruments — ARM / ARM_CONTEXT / BUS)" },
{ code: "NOT_FOUND", exit: 3, when: "`runs show <id>` / `lineage <name>` / an input file is absent" },
{ code: "RUNTIME_UNAVAILABLE", exit: 4, when: "bun/chdb is needed but could not be loaded" },
{ code: "PAYMENT_REQUIRED", exit: 5, when: "a remote verb returned a 402/Offer (surfaced as data)" },
{ code: "NO_AUTHOR", exit: 6, when: "`day` (default or `--no-author`): a handshake document was not staged in --dir and no author wait was declared" },
{ code: "HANDSHAKE_ABANDONED", exit: 6, when: "`day --await-author`: no author reply (plans-0/revision/pass) within --max-wait" },
{
code: "PAPER_REFUSED",
exit: 7,
when: "`paper`: the session refused fail-closed (unreachable/degraded IB Gateway, unresolved contract, absent/stale feed, tripped kill-switch, unbounded limits, a non-paper mode, a real-money port, or a gateway reporting a non-PAPER account)",
},
{ code: "SIGINT", exit: 130, when: "interrupted (Ctrl-C)" },
];
/** One command's own usage + flag inventory — static help data (no heavy import). */
interface SubHelp {
readonly name: string;
readonly usage: string;
readonly purpose: string;
readonly flags: readonly { flag: string; purpose: string }[];
/** Nested subcommands (only `runs`), listed when the bare parent is asked for help. */
readonly subcommands?: readonly { name: string; purpose: string }[];
/** Free-form explanatory paragraphs rendered under a `notes:` header (e.g. `register`'s disclosure/storage/re-register behavior). */
readonly notes?: readonly string[];
/** Free-form protocol description paragraphs (`agent` / `mcp`) — what the verb is and how to drive it. */
readonly protocol?: readonly string[];
/**
* The verb's request operations (only `agent`) — enumerated from the actual handler switch
* (`src/cli/commands/agent.ts`). The discovery op (`catalog`) is authored first. `args` names the
* request-envelope fields each op reads beyond `op`.
*/
readonly ops?: readonly { op: string; args: string; purpose: string }[];
/** A copy-pasteable example (only `agent`): the command line, the piped request, and the response shape. */
readonly example?: { readonly title: string; readonly command: string; readonly output: string };
}
/**
* Per-command help tables. The flag lists are authored to match each command's own parser
* (`src/cli/commands/{grade,registry,frame,lang}.ts`) — this is the discoverability surface the
* top-level usage cannot carry. Required flags are marked `(required)`.
*/
const SUBHELP: Readonly<Record<string, SubHelp>> = {
run: {
name: "run",
usage: "kestrel run --bus <p> --plans <p> --fill <m> --r-usd <n> [flags]",
purpose: "grade a plan document against a recorded bus, print the report, and auto-record it",
flags: [
{ flag: "--bus <p>", purpose: "the recorded market bus to grade against (required)" },
{ flag: "--plans <p>", purpose: "the plan document to arm — .kestrel, or .md with ```kestrel blocks (required)" },
{ flag: "--fill <m>", purpose: "fill model: strict-cross-v1 | maker-fair-v1 (required)" },
{ flag: "--r-usd <n>", purpose: "dollar value of 1R, a positive number (required)" },
{ flag: "--tau-hours <n>", purpose: "fair time-to-expiry in hours (maker-fair calibration)" },
{ flag: "--hazard-params <p>", purpose: "episode-sigmoid params JSON (only with --fill maker-fair-v1)" },
{ flag: "--out <p>", purpose: "also write the EpisodeReport JSON to this path" },
{ flag: "--db <p>", purpose: "registry path to record into (default data/kestrel.db)" },
{ flag: "--no-record", purpose: "grade only — do NOT write the run to the registry" },
],
},
day: {
name: "day",
usage: "kestrel day --bus <p> --dir <d> --fill <m> --r-usd <n> [flags]",
purpose: "run a stepped/wake session (wake handshake + document supersession), and auto-record it",
flags: [
{ flag: "--bus <p>", purpose: "the recorded market bus to grade against (required)" },
{ flag: "--dir <d>", purpose: "handshake dir the stepped session authors plans into (plans-N.kestrel) (required)" },
{ flag: "--fill <m>", purpose: "fill model: strict-cross-v1 | maker-fair-v1 (required)" },
{ flag: "--r-usd <n>", purpose: "dollar value of 1R, a positive number (required)" },
{ flag: "--wakes <HH:MM,…>", purpose: "explicit wake clock times (ET) to step the session at" },
{ flag: "--await-author", purpose: "block for an out-of-band author at each handshake, up to --max-wait (default: refuse promptly on a missing document)" },
{ flag: "--max-wait <s>", purpose: "seconds to wait for a handshake reply under --await-author (positive number, default 900); supplying it implies --await-author" },
{ flag: "--no-author", purpose: "explicit: no author will reply — documents must be pre-staged in --dir (same prompt exit-6 refusal as the default)" },
{ flag: "--structural", purpose: "use structural-cadence wakes" },
{ flag: "--tau-hours <n>", purpose: "fair time-to-expiry in hours (maker-fair calibration)" },
{ flag: "--hazard-params <p>", purpose: "episode-sigmoid params JSON (only with --fill maker-fair-v1)" },
{ flag: "--out <p>", purpose: "also write the EpisodeReport JSON to this path" },
{ flag: "--db <p>", purpose: "registry path to record into (default data/kestrel.db)" },
{ flag: "--no-record", purpose: "grade only — do NOT write the run to the registry" },
],
},
paper: {
name: "paper",
usage:
"kestrel paper --instrument <s> --plans <p> --session-date <d> --r-usd <n> --max-order-qty <n> --max-position-qty <n> --max-notional-usd <n> [flags]",
purpose:
"run a PAPER session: a LIVE IB Gateway feed + the venue's paper gate under the same engine `run`/`day` drive over recorded tape. Simulated fills; NEVER live money (exit 7 refuses, loudly): there is no --live flag, KESTREL_IBKR_MODE=live is REFUSED not honoured, IB's real-money ports (4001/7496) are refused, and the account barrier classifies the account the gateway ITSELF reports (never the pinned claim) — a gateway that reports a non-PAPER account (live U…, or one that cannot be classified), OR a pinned --account that does not match the gateway's report, is refused and its socket closed BEFORE any tape, gate, or order exists, on any port",
flags: [
{ flag: "--instrument <s>", purpose: "the underlier symbol to trade, e.g. SPY (required)" },
{ flag: "--plans <p>", purpose: "the standing document to arm — .kestrel, or .md with ```kestrel blocks (required)" },
{ flag: "--session-date <d>", purpose: "the session's calendar token, YYYY-MM-DD (required)" },
{ flag: "--r-usd <n>", purpose: "dollar value of 1R, a positive number (required)" },
{ flag: "--max-order-qty <n>", purpose: "L0 ceiling: max contracts/shares per order (required — no default; an absent ceiling is not a ceiling)" },
{ flag: "--max-position-qty <n>", purpose: "L0 ceiling: max absolute net position per leg (required)" },
{ flag: "--max-notional-usd <n>", purpose: "L0 ceiling: max px×qty×multiplier per order, in dollars (required)" },
{ flag: "--budget-usd <n>", purpose: "bounded-risk budget: size × max_loss ≤ budget (default: --max-notional-usd)" },
{ flag: "--expiry <YYYYMMDD>", purpose: "the option chain expiry to subscribe to (needs --spot). Absent ⇒ an equity-only tape" },
{ flag: "--spot <n>", purpose: "the OBSERVED underlier price the strike window centres on — required with --expiry; a centre is never guessed" },
{ flag: "--half-width <n>", purpose: "LISTED strikes each side of the ATM one" },
{ flag: "--tau-hours <n>", purpose: "fair time-to-expiry in hours (injected — never read off a clock)" },
{ flag: "--live-market-data", purpose: "DECLARE that this gateway serves REAL-TIME data (asks IB for type 1). Absent ⇒ every price anchor is stamped `unknown` and every order is REFUSED — the session perceives but cannot price" },
{ flag: "--tolerance <n>", purpose: "reconciliation tolerance, absolute qty per leg (default 0 — exact match)" },
{ flag: "--cycles <n>", purpose: "how many live pump cycles to run (default 60)" },
{ flag: "--pump-ms <n>", purpose: "host park between pump cycles in ms (default 1000)" },
{ flag: "--host <h>", purpose: "IB Gateway host (default 127.0.0.1 / $KESTREL_IBKR_HOST) — a locally-running gateway, not a credential" },
{ flag: "--port <n>", purpose: "IB Gateway API port (default 4002 = IB Gateway PAPER / $KESTREL_IBKR_PORT). IB's REAL-MONEY ports (4001 IB Gateway live, 7496 TWS live) are REFUSED — there is no proceed-anyway flag. A live gateway on any OTHER port is caught by the account barrier, which classifies the account the gateway itself reports (never the port, never a pinned claim)" },
{ flag: "--client-id <n>", purpose: "unique API client id per gateway instance (default 0 / $KESTREL_IBKR_CLIENT_ID)" },
{ flag: "--account <a>", purpose: "pin an account id (default: discovered from the gateway handshake). The pin is a CLAIM, never the barrier: the gateway's OWN reported account must be a PAPER account (DU/DF) AND the pin must EQUAL it — a reported live U… account, an unclassifiable one, or a pin that does not match the gateway's report is REFUSED and the socket closed. Redacted in every diagnostic" },
{ flag: "--out <p>", purpose: "also write the session Bus + order ledger JSON to this path" },
],
},
runs: {
name: "runs",
usage: "kestrel runs <list|show|compare> [flags]",
purpose: "query the run registry",
flags: [],
subcommands: [
{ name: "list", purpose: "query recorded runs (see `kestrel runs list --help`)" },
{ name: "show", purpose: "show one recorded run + its plans (see `kestrel runs show --help`)" },
{ name: "compare", purpose: "diff two hosted `sim` receipts (see `kestrel runs compare --help`)" },
],
},
"runs list": {
name: "runs list",
usage: "kestrel runs list [flags]",
purpose: "query recorded runs, optionally filtered",
flags: [
{ flag: "--session-date <d>", purpose: "filter to one session date" },
{ flag: "--fill <m>", purpose: "filter to one fill model" },
{ flag: "--lineage <n>", purpose: "filter to one plan lineage" },
{ flag: "--db <p>", purpose: "registry path to read (default data/kestrel.db)" },
],
},
"runs show": {
name: "runs show",
usage: "kestrel runs show <id> [flags]",
purpose:
"show one run; <id> may be a 12-char prefix — a local `run`/`day` run (+ its plans), or a hosted `sim` receipt (+ its shareable proof URL). The hosted lineage resolves node-light (no Bun needed).",
flags: [{ flag: "--db <p>", purpose: "registry path to read; the hosted receipts sit beside it (default data/kestrel.db)" }],
},
"runs compare": {
name: "runs compare",
usage: "kestrel runs compare <runA> <runB> [flags]",
purpose:
"diff two hosted `sim` receipts (by operation-id or 12-char prefix): which plan/strategy each used + the graded activity deltas (orders, fills, realized P&L), computed B − A. Node-light (no Bun) — the returning user's \"did my tweak help?\" verb.",
flags: [{ flag: "--db <p>", purpose: "registry path; the hosted receipts sit beside it (default data/kestrel.db)" }],
},
lineage: {
name: "lineage",
usage: "kestrel lineage <name> [flags]",
purpose: "show a plan lineage (the graded runs sharing a plan name)",
flags: [{ flag: "--db <p>", purpose: "registry path to read (default data/kestrel.db)" }],
},
leaderboard: {
name: "leaderboard",
usage: "kestrel leaderboard [flags]",
purpose: "the ranked leaderboard over recorded runs",
flags: [
{ flag: "--since <ms>", purpose: "only runs at/after this epoch-ms" },
{ flag: "--mode <m>", purpose: "leaderboard mode" },
{ flag: "--db <p>", purpose: "registry path to read (default data/kestrel.db)" },
],
},
frame: {
name: "frame",
usage: "kestrel frame [<input.json>|-] [flags]",
purpose: "render a Frame from a fixture/snapshot JSON (path arg, --input, or - / stdin)",
flags: [
{ flag: "--input <p>", purpose: "the fixture JSON path (alternative to the positional)" },
{ flag: "--kind <briefing|wake>", purpose: "override the kind inference (presence of wakeIndex ⇒ wake)" },
{
flag: "--view <name[:panes]>",
purpose:
"render under a View: a shipped named View, or inline panes `name:pane[ arg…][,pane…]` (unknown/malformed ⇒ loud refusal, exit 2)",
},
{
flag: "--seat <pm|strategist|watcher>",
purpose: "acting pod seat — reads its founder View when one exists (precedence: --view > seat founder View > phase default)",
},
{ flag: "--seat-views founder", purpose: "explicit founder-View opt-in assertion (requires --seat)" },
],
},
percept: {
name: "percept",
usage: "kestrel percept [<input.json>|-] [flags]",
purpose: "render a Frame (alias of frame)",
flags: [
{ flag: "--input <p>", purpose: "the fixture JSON path (alternative to the positional)" },
{ flag: "--kind <briefing|wake>", purpose: "override the kind inference (presence of wakeIndex ⇒ wake)" },
{
flag: "--view <name[:panes]>",
purpose:
"render under a View: a shipped named View, or inline panes `name:pane[ arg…][,pane…]` (unknown/malformed ⇒ loud refusal, exit 2)",
},
{
flag: "--seat <pm|strategist|watcher>",
purpose: "acting pod seat — reads its founder View when one exists (precedence: --view > seat founder View > phase default)",
},
{ flag: "--seat-views founder", purpose: "explicit founder-View opt-in assertion (requires --seat)" },
],
},
sim: {
name: "sim",
usage: "kestrel sim [<scenario-slug>] [--plans <file>] [--budget <amount>]",
purpose:
"run a curated hosted market scenario, free — bare `sim` prints the menu; a lowercase scenario slug mints an anonymous trial, runs the hosted sim, and prints the graded story + a shareable proof URL",
notes: [
"author a --plans document: `kestrel card first-plan` (offline walkthrough); `kestrel card` for the full language card",
"`--budget <amount>` declares a spend ceiling (USD) you OPT INTO — a metered draw past it pauses at the paid boundary and prints the settleable Offer (price, scope, settle URL, expiry) with exit 5 PAYMENT_REQUIRED. The CLI never auto-pays; payment stays an explicit, separate step.",
],
flags: [
{ flag: "--plans <file>", purpose: "the Kestrel document to arm — .kestrel, .md (```kestrel blocks), or - (stdin); default: a labeled stand-down demo" },
{ flag: "--budget <amount>", purpose: "declare a spend ceiling in USD (a HONORED CAP) — reaches the paid boundary as DATA (exit 5); the CLI never auto-pays" },
{ flag: "--copy-token <t>", purpose: "an OPAQUE pass-through nonce forwarded verbatim to the platform at mint time (also KESTREL_COPY_TOKEN); the CLI never interprets it" },
],
},
prove: {
name: "prove",
usage: "kestrel prove [<scenario-slug>] [--plans <file>] [--copy-token <t>] [--no-residency]",
purpose:
"the zero-credential front door — bare `prove` needs no key, no config, and no prompt: it runs a default free scenario against the hosted anonymous trial, prints the graded story + a shareable proof URL, and exhausts to a residency snippet (how to keep the capability)",
notes: [
"Bare `prove` selects a pinned default free scenario and succeeds with exit 0 + a real proof URL — it never falls through to a menu or a prompt.",
"`prove <slug>` runs that curated scenario; `--plans <file>` authors a strategy; `--no-residency` suppresses the persist-me snippet.",
"author a --plans document: `kestrel card first-plan` (offline walkthrough); `kestrel card` for the full language card.",
],
flags: [
{ flag: "--plans <file>", purpose: "the Kestrel document to arm — .kestrel, .md (```kestrel blocks), or - (stdin); default: a labeled stand-down demo" },
{ flag: "--copy-token <t>", purpose: "an OPAQUE pass-through nonce forwarded verbatim to the platform at mint time (also KESTREL_COPY_TOKEN); the CLI never interprets it" },
{ flag: "--no-residency", purpose: "do NOT print the residency (persist-me) snippet after the proof" },
],
},
replay: {
name: "replay",
usage: "kestrel replay <proofId> [--copy-token <t>]",
purpose:
"reproduce a proof this machine produced (re-runs the recorded scenario on a fresh anonymous trial and asserts the new grade is structurally reproducible + its signature re-verifies), or — for any other proof — degrade honestly to verifying the published proof",
notes: [
"A proof id present in your identity-bound run ledger (~/.kestrel/hosted-runs.jsonl, the store `runs list` reads — found from any directory) is REPRODUCED; any other id degrades to `verify` with a one-line notice.",
"No-leak: the ledger never stores the strategy source, so a stranger's proof can never be re-executed — only verified.",
],
flags: [
{ flag: "--copy-token <t>", purpose: "an OPAQUE pass-through nonce forwarded verbatim to the platform (also KESTREL_COPY_TOKEN); the CLI never interprets it" },
],
},
verify: {
name: "verify",
usage: "kestrel verify <https://kestrel.markets/proof/<id> | <proofId>> [--copy-token <t>]",
purpose:
`zero-trust re-verification of a PUBLISHED proof: fetch the proof + the published verify keys independently, and re-check the Ed25519 signature over \`${GRADE_SIGN_PREFIX}.<root>\` LOCALLY — it NEVER trusts the proof body's own \`verification.verified\``,
notes: [
"Verdicts (data on stdout): VERIFIED (a held key verifies the signature) · UNVERIFIED (a held key does not — tampering/corruption) · KEY_RETIRED (the proof's kid is not in the published set) · NOT_FOUND. Exit 0 only for VERIFIED.",
"Mints no trial — two GETs, no funnel touch.",
],
flags: [
{ flag: "--copy-token <t>", purpose: "an OPAQUE pass-through nonce forwarded verbatim on the read (also KESTREL_COPY_TOKEN); the CLI never interprets it" },
],
},
certify: {
name: "certify",
usage: "kestrel certify <https://kestrel.markets/proof/<id> | <proofId>> [--copy-token <t>]",
purpose:
"open recomputation (\"don't trust us — run it\", gate G10): fetch the proof's evidence bundle, re-project the Blotter LOCALLY with the shipped projector, and assert byte-identical reproduction of the hosted certified result — the determinism leg (L1 minimum), never a signature check",
notes: [
"Verdicts (data on stdout): REPRODUCED (the local re-projection reproduces the published Blotter byte-for-byte AND the local projector is pure) · MISMATCH (the local re-projection differs — genuine divergence) · NON_DETERMINISTIC (the local projector is not a pure function of the bus, fail-closed) · NOT_FOUND (no re-projectable free-tier bundle for this id). Exit 0 only for REPRODUCED.",
"This is the L1 determinism leg (byte-identical Blotter re-projection), clearly labeled; full judged-Grade recomputation follows by L3. Distinct from `verify` (which re-checks the Ed25519 signature); `certify` re-runs the computation.",
],
flags: [
{ flag: "--copy-token <t>", purpose: "an OPAQUE pass-through nonce forwarded verbatim on the read (also KESTREL_COPY_TOKEN); the CLI never interprets it" },
],
},
register: {
name: "register",
usage: "kestrel register [--name <n>] [--scopes <s,s>] [--no-git-identity] [--api <url>]",
purpose: "self-register as an autonomous agent and store the minted capability (PLAT-ADR-0021 tier 1, no human in the loop)",
notes: [
"Mints a durable capability from the managed platform — the middle rung between an anonymous trial and a human",
"account; no human is in the loop. It always routes to the managed remote (registration needs the network).",
"Before sending, it DISCLOSES to stderr exactly what it will POST (the endpoint + any claimed git identity) and",
"that the git identity is UNVERIFIED / freely spoofable — pass --no-git-identity to omit it.",
"On success the returned capability is stored at ~/.kestrel/credentials.json (dir 0700, file 0600; override the",
"home dir with $KESTREL_HOME). Subsequent --api calls present it instead of self-minting an anonymous trial.",
"--scopes is a comma-separated scope REQUEST forwarded to the platform as `scope_request`; the platform decides",
"what to grant and the GRANTED scopes are echoed on success (the CLI enforces no scope list of its own).",
"Re-registering POSTs again and OVERWRITES the stored credential for that API host with the freshly minted one",
"(the credential is host-scoped by its `api` field).",
],
flags: [
{ flag: "--name <n>", purpose: "an optional display name to send with the registration" },
{ flag: "--scopes <s,s>", purpose: "a comma-separated scope REQUEST (sent as `scope_request`); the platform decides what to grant" },
{ flag: "--no-git-identity", purpose: "do NOT send the claimed (unverified) git user.name/user.email" },
{ flag: "--api <url>", purpose: "the managed base to register against (`--api default` ⇒ api.kestrel.markets; also KESTREL_API)" },
],
},
whoami: {
name: "whoami",
usage: "kestrel whoami",
purpose: "inspect the stored credential without hand-parsing credentials.json",
notes: [
"Reads ~/.kestrel/credentials.json ($KESTREL_HOME-overridable) and prints the identity (agent_id / subject),",
"the API host the credential is bound to, the token type, the granted scopes, the expiry with a display-clock",
"STATUS (valid / expiring / EXPIRED — display only, never a token or Session clock), whether an Ed25519 keypair",
"is enrolled, and the on-disk file path. Pure-local: ZERO network. No stored credential → exit 3 (NOT_FOUND)",
"naming how to `register`. When the credential is near/after expiry the status line points at `kestrel refresh`.",
],
flags: [],
},
secrets: {
name: "secrets",
usage: "kestrel secrets set <KEY> [--stdin] | kestrel secrets list | kestrel secrets unset <KEY> | kestrel secrets path",
purpose: "manage operator/BYOK secrets in the owner-only ~/.kestrel/.env store",
notes: [
"The store is a plain dotenv file at ~/.kestrel/.env ($KESTREL_HOME-overridable), dir 0700 / file 0600, written",
"atomically. Resolution precedence is process.env FIRST, the file as the fallback — so CI/containers can inject",
"a secret without touching disk. Pure-local: ZERO network.",
"`set` reads the value from an interactive prompt (echo suppressed) or from stdin with --stdin. A value passed",
"POSITIONALLY is REFUSED (code SECRET_VALUE_IN_ARGV, exit 2) and nothing is written: argv leaks into shell",
"history, `ps` output, and process logs. A non-interactive session without --stdin is likewise refused",
"(SECRET_NO_INPUT) rather than hanging or storing an empty value.",
"A key name must be a canonical uppercase env name [A-Z_][A-Z0-9_]* or it is refused (SECRET_INVALID_KEY_NAME),",
"and a *LIVE* broker key name — any `LIVE` name segment, matched case-insensitively and segment-bounded",
"(/(^|_)LIVE(_|$)/i, so ALPACA_LIVE_KEY is caught but DELIVERY_URL is not) — is refused (SECRET_LIVE_KEY_REFUSED)",
"with a wrangler pointer: the ~/.kestrel/.env store is PAPER-ONLY (OSS-ADR-0054 §5); live keys belong in",
"`wrangler secret`. Both refuse fail-closed, BEFORE any value is read, so nothing is written.",
"`list` prints NAMES ONLY — no output path of this command ever prints a value. `unset` on an absent key is",
"exit 3 (NOT_FOUND), never a silent success. `path` prints the store location.",
"The SAME store is exposed on the local MCP face (kestrel.secrets.set/unset/list) so an agent can self-install",
"its BYOK keys once; there, too, list is names-only and NO face ever returns a stored value.",
],
flags: [{ flag: "--stdin", purpose: "read the value for `set` from stdin instead of prompting (one trailing newline is stripped)" }],
},
refresh: {
name: "refresh",
usage: "kestrel refresh",
purpose: "renew the stored durable capability before it lapses",
notes: [
"Projects the platform's `POST /capabilities/refresh` primitive onto the CLI face: a durable capability lives",
"1h, and CLI-only fleets previously had no renewal path (renewal existed only over MCP/HTTP). Authenticated by",
"the CURRENT stored capability, host-scoped to the credential's own `api`. The fresh token is minted SERVER-SIDE",
"— the CLI adds NO client RNG and no client wall-clock into the token (determinism doctrine) — then overwrites",
"the stored credential IN PLACE (preserving agent_id / subject / keypair enrollment / claimed attribution).",
"No stored credential → exit 3 (NOT_FOUND) naming how to `register`; a server refusal (a capability past its",
"refresh window, a revoked family) surfaces the server's problem+json detail, never a bare status.",
],
flags: [],
},
agent: {
name: "agent",
usage: "kestrel agent [--api <url>] (reads JSONL requests on stdin, writes JSONL responses on stdout)",
purpose: "the machine/agent face — a JSONL request/response protocol projecting the Kestrel SDK",
protocol: [
"Read one JSON request envelope per line on stdin (`{ \"op\": <name>, ...args }`); each is forwarded to the",
"Kestrel SDK and answered with one line on stdout: `{ \"v\": \"kestrel.agent/v1\", \"kind\": <op>, \"value\": <payload> }`.",
"stdout is a PURE protocol channel (only versioned JSONL); ALL diagnostics go to stderr.",
"A refusal is a typed line on STDERR — `{ \"v\": \"kestrel.agent/v1\", \"kind\": \"refusal\", \"code\": <stable>, \"message\": ... }`",
"(match on `code`, not prose) — and the process exits nonzero; the payload channel never carries a fabricated ok.",
"Transport: no --api ⇒ LOCAL (in-process, no network); `--api <url>` / `--api default` / KESTREL_API ⇒ the managed",
"remote. The request stream and the protocol objects are byte-identical across transports.",
"Start with `describe` (needs no session) to learn every op's ARGUMENT + response shapes — the `{kind:…}` unions a",
"`subject` / `advance.response` must carry, including the exact accepted field name `document`; then `catalog` to discover subjects.",
"The session verbs (start/advance/revise/submit/resume/finalize) require an `openSession` first and refuse (code `no-session`)",
"otherwise. An unknown op refuses (code `unknown-op`) and names the valid ops; a malformed argument object refuses (code `bad-arg`) and NAMES the expected shape.",
"Exit codes: 0 when every request line was answered; 2 (USAGE) on a malformed line, an unknown op, or an empty request",
"stream; 1 (GENERIC) on a domain refusal from the SDK mid-stream. (Full taxonomy: `kestrel --help`.)",
],
ops: [
{ op: "describe", args: "", purpose: "emit this protocol's own op/argument/response shapes (self-description) — the `{kind:…}` shapes for `subject` and `advance.response`, incl. the exact accepted field name `document`; needs no session (call it first)" },
{ op: "catalog", args: "", purpose: "list the managed capability catalog — the subject-discovery op; needs no session" },
{ op: "validate", args: "document", purpose: "validate a Kestrel document string" },
{ op: "openSession", args: "subject, document?", purpose: "open a Session over a subject; pass `document` (Kestrel plan text) as the customer strategy the managed backend requires (a bare catalog openSession is refused by the author-no-strategy fence — ADR-0012); emits { gated:false, sessionId } or { gated:true, payment } (a 402/Offer is DATA)" },
{ op: "start", args: "", purpose: "start the opened Session (requires a prior openSession)" },
{ op: "advance", args: "response", purpose: "advance the Session with an authored response" },
{ op: "revise", args: "response", purpose: "revise the current turn with an authored response" },
{ op: "submit", args: "response", purpose: "submit a bound response" },
{ op: "resume", args: "after?", purpose: "replay the Session event stream from an optional cursor" },
{ op: "finalize", args: "", purpose: "finalize the Session" },
{ op: "grade", args: "blotters[]", purpose: "grade the given blotters" },
{ op: "artifact", args: "ref", purpose: "fetch an artifact by ref" },
{ op: "resumeOperation", args: "operationId, after?", purpose: "resume a durable managed Operation from its handle" },
],
example: {
title: "discover the catalog (LOCAL — no network):",
command: "echo '{\"op\":\"catalog\"}' | kestrel agent",
output: '{"v":"kestrel.agent/v1","kind":"catalog","value":[ /* CatalogEntry[] */ ]}',
},
flags: [
{ flag: "--api <url>", purpose: "route to the managed remote transport (`--api default` ⇒ api.kestrel.markets; also KESTREL_API); omit for local" },
],
},
mcp: {
name: "mcp",
usage: "kestrel mcp [--local | --api <url>] (serves MCP — newline-delimited JSON-RPC 2.0 — on stdin/stdout until stdin ends)",
purpose: "the MCP stdio server — the same tool/resource surface the hosted MCP server projects, as a local stdio drop-in",
protocol: [
"Speaks MCP (JSON-RPC 2.0, one frame per line) over stdin/stdout: initialize, tools/list, tools/call,",
"resources/list, resources/read. One tool per SDK verb (validate, the Session lifecycle, grade, operation",
"resume); every tool result carries the SDK's protocol object VERBATIM in `structuredContent`.",
"Configure an MCP client with: { \"command\": \"npx\", \"args\": [\"kestrel.markets\", \"mcp\"] }.",
"Transport: DEFAULT is the managed remote (api.kestrel.markets; `--api <url>` selects another base) — pure",
"fetch, runs under plain node. `--local` serves the in-process engine instead (requires the Bun runtime;",
"refuses loud, exit 4, without one). `--local` with `--api` is a contradiction and refuses (USAGE).",
"stdout is the MCP wire ONLY; the process serves until stdin ends (the MCP client owns the lifecycle).",
],
flags: [
{ flag: "--local", purpose: "serve the in-process engine instead of the managed remote (requires bun; contradicts --api)" },
{ flag: "--api <url>", purpose: "the managed remote base (`--api default` ⇒ api.kestrel.markets; also KESTREL_API); this is the default transport" },
],
},
parse: {
name: "parse",
usage: "kestrel parse <file> [--arm] [--bus <tape> | --instruments <SYM[,SYM…]>]",
purpose: "parse + validate a plan document",
notes: [
"parse-green is not arm-green: --arm also runs the engine's arm-time checks (series vocabulary, envelope) — the refusal shown is byte-identical to what a run raises",
"learn the language: `kestrel card` (the shipped agent language card, offline; `kestrel card first-plan` for a walkthrough)",
],
flags: [
{ flag: "--arm", purpose: "also validate at the ARM tier (needs a session context via --bus or --instruments; refuses fail-closed without one)" },
{ flag: "--bus <tape>", purpose: "derive the session instruments from the tape's META, exactly as `run` resolves them (implies --arm)" },
{ flag: "--instruments <SYM[,SYM…]>", purpose: "name the session instruments directly, validated as option-underliers (implies --arm)" },
],
},
validate: {
name: "validate",
usage: "kestrel validate <file> [--arm] [--bus <tape> | --instruments <SYM[,SYM…]>]",
purpose: "parse + validate a plan document (alias of parse)",
notes: [
"parse-green is not arm-green: --arm also runs the engine's arm-time checks (series vocabulary, envelope) — the refusal shown is byte-identical to what a run raises",
"learn the language: `kestrel card` (the shipped agent language card, offline; `kestrel card first-plan` for a walkthrough)",
],
flags: [
{ flag: "--arm", purpose: "also validate at the ARM tier (needs a session context via --bus or --instruments; refuses fail-closed without one)" },
{ flag: "--bus <tape>", purpose: "derive the session instruments from the tape's META, exactly as `run` resolves them (implies --arm)" },
{ flag: "--instruments <SYM[,SYM…]>", purpose: "name the session instruments directly, validated as option-underliers (implies --arm)" },
],
},
print: {
name: "print",
usage: "kestrel print <file>",
purpose: "canonical re-print of a plan document (print(parse(text)) is byte-stable)",
notes: ["learn the language: `kestrel card` (the shipped agent language card, offline; `kestrel card first-plan` for a walkthrough)"],
flags: [],
},
card: {
name: "card",
usage: "kestrel card [<topic>] [--json]",
purpose: "print the shipped agent language card / first-steps walkthroughs from the INSTALLED package, offline",
notes: [
"Bare `kestrel card` prints the compact agent language card (docs/public/card.md) — the version-coupled",
"reference an agent loads before authoring Kestrel. It resolves from the installed package: zero network,",
"works offline from any directory.",
"`--json` (bare) emits the machine twin card.json verbatim; `kestrel card <topic> --json` wraps a topic's",
"markdown in a { schema, topic, markdown } envelope.",
"A `<topic>` prints docs/public/<topic>.md — start with `first-plan` (author your first document), then",
"`first-wake`, `first-frame`, `first-grade`; `adopt` walks the fill→adopt→exit escape hatch (carry a held",
"leg across a wake with `ARM … foreach held leg`); `overview`, `lexical-core`, `grammar-fences`,",
"`cli-reference`, `api-reference`, `agent-protocol`, `capability-truth`, and `status` go deeper. An unknown",
"topic is refused (exit 2) and the refusal lists every valid topic.",
],
flags: [
{ flag: "--json", purpose: "bare: emit card.json (the machine twin) verbatim; with a topic: a { schema, topic, markdown } envelope" },
],
},
};
const BOLD = "[1m";
const RESET = "[0m";
/** `version` — prints `kestrel <version>`; in json `{schema, version}`. Exit 0. */
export function versionCommand(ctx: OutputCtx): number {
if (ctx.mode === "json") {
process.stdout.write(JSON.stringify({ schema: "kestrel.version/v1", version: VERSION }) + "\n");
} else if (ctx.mode === "text") {
process.stdout.write(`kestrel ${VERSION}\n`);
} else {
process.stdout.write(`kestrel ${VERSION}\n`);
}
return 0;
}
/**
* Resolve the command path (`["runs","list"]`, `["run"]`, …) a `--help`/`help` request targets:
* strip a leading `help` word, then take the leading NON-flag tokens. Returns the {@link SUBHELP}
* key if one exists (joining `runs`+sub), else `undefined` (→ the top-level usage).
*/
function helpKeyFor(rest: readonly string[]): string | undefined {
const words = rest[0] === "help" ? rest.slice(1) : rest;
const path: string[] = [];
for (const w of words) {
if (w.startsWith("-")) break;
path.push(w);
}
const [a, b] = path;
if (a === undefined) return undefined;
if (a === "runs") {
if (b === "list" || b === "show" || b === "compare") return `runs ${b}`;
return "runs";
}
return a in SUBHELP ? a : undefined;
}
/**
* `help` — the usage inventory. With no command word it prints the top-level usage; a command word
* (`kestrel <cmd> --help` or `kestrel help <cmd>`) prints that command's own usage + flags. Exit 0.
*/
export function helpCommand(ctx: OutputCtx, rest: readonly string[] = []): number {
const key = helpKeyFor(rest);
if (key !== undefined) return renderCommandHelp(ctx, SUBHELP[key]!);
return renderTopHelp(ctx);
}
/** The top-level usage inventory: commands, global flags, and the exit-code taxonomy. */
function renderTopHelp(ctx: OutputCtx): number {
if (ctx.mode === "json") {
process.stdout.write(
JSON.stringify({
schema: "kestrel.help/v1",
version: VERSION,
commands: COMMANDS.map((c) => ({ name: c.name, args: c.args, purpose: c.purpose })),
globalFlags: GLOBALS.map((g) => ({ flag: g.flag, purpose: g.purpose })),
exitCodes: EXIT_CODES.map((e) => ({ code: e.code, exit: e.exit, when: e.when })),
}) + "\n",
);
return 0;
}
const color = ctx.mode === "human" && ctx.color;
const header = (s: string): string => (color ? `${BOLD}${s}${RESET}` : s);
const lines: string[] = [];
lines.push(`kestrel ${VERSION} — a typed, token-efficient language + runtime for agentic trading`);
lines.push("");
lines.push(header("usage:"));
lines.push(" kestrel <command> [args] [--json|--format <m>|--agent] [--color <m>] [--no-color]");
lines.push(" kestrel <command> --help per-command usage + its own flags");
lines.push("");
lines.push(header("commands:"));
const wName = Math.max(...COMMANDS.map((c) => `${c.name} ${c.args}`.length));
for (const c of COMMANDS) {
const left = `${c.name} ${c.args}`.trimEnd();
lines.push(` ${left.padEnd(wName)} ${c.purpose}`);
}
lines.push("");
lines.push(header("global flags:"));
const wFlag = Math.max(...GLOBALS.map((g) => g.flag.length));
for (const g of GLOBALS) {
lines.push(` ${g.flag.padEnd(wFlag)} ${g.purpose}`);
}
lines.push("");
lines.push(header("exit codes:"));
const wCode = Math.max(...EXIT_CODES.map((e) => `${e.exit} ${e.code}`.length));
for (const e of EXIT_CODES) {
lines.push(` ${`${e.exit} ${e.code}`.padEnd(wCode)} ${e.when}`);
}
lines.push("");
lines.push("learn the language (offline, shipped in this package): `kestrel card` — `kestrel card first-plan` for a first-steps walkthrough");
process.stdout.write(lines.join("\n") + "\n");
return 0;
}
/** One command's own usage + flags. json = structured; text/human = a printed usage. Exit 0. */
function renderCommandHelp(ctx: OutputCtx, spec: SubHelp): number {
if (ctx.mode === "json") {
process.stdout.write(
JSON.stringify({
schema: "kestrel.help.command/v1",
version: VERSION,
command: spec.name,
usage: spec.usage,
purpose: spec.purpose,
flags: spec.flags.map((f) => ({ flag: f.flag, purpose: f.purpose })),
...(spec.subcommands !== undefined
? { subcommands: spec.subcommands.map((s) => ({ name: s.name, purpose: s.purpose })) }
: {}),
...(spec.notes !== undefined ? { notes: spec.notes.join(" ") } : {}),
...(spec.protocol !== undefined ? { protocol: spec.protocol.join(" ") } : {}),
...(spec.ops !== undefined
? { ops: spec.ops.map((o) => ({ op: o.op, args: o.args, purpose: o.purpose })) }
: {}),
...(spec.example !== undefined
? { example: { title: spec.example.title, command: spec.example.command, output: spec.example.output } }
: {}),
}) + "\n",
);
return 0;
}
const color = ctx.mode === "human" && ctx.color;
const header = (s: string): string => (color ? `${BOLD}${s}${RESET}` : s);
const lines: string[] = [];
lines.push(`kestrel ${spec.name} — ${spec.purpose}`);
lines.push("");
lines.push(header("usage:"));
lines.push(` ${spec.usage}`);
if (spec.notes !== undefined && spec.notes.length > 0) {
lines.push("");
lines.push(header("notes:"));
for (const n of spec.notes) lines.push(` ${n}`);
}
if (spec.protocol !== undefined && spec.protocol.length > 0) {
lines.push("");
lines.push(header("protocol:"));
for (const p of spec.protocol) lines.push(` ${p}`);
}
if (spec.ops !== undefined && spec.ops.length > 0) {
lines.push("");
lines.push(header("operations (op → payload; first is the discovery op):"));
const wOp = Math.max(...spec.ops.map((o) => (o.args.length > 0 ? `${o.op} { ${o.args} }` : o.op).length));
for (const o of spec.ops) {
const left = o.args.length > 0 ? `${o.op} { ${o.args} }` : o.op;
lines.push(` ${left.padEnd(wOp)} ${o.purpose}`);
}
}
if (spec.example !== undefined) {
lines.push("");
lines.push(header("example:"));
lines.push(` ${spec.example.title}`);
lines.push(` $ ${spec.example.command}`);
lines.push(` ${spec.example.output}`);
}
if (spec.subcommands !== undefined && spec.subcommands.length > 0) {
lines.push("");
lines.push(header("subcommands:"));
const wSub = Math.max(...spec.subcommands.map((s) => s.name.length));
for (const s of spec.subcommands) {
lines.push(` ${s.name.padEnd(wSub)} ${s.purpose}`);
}
}
if (spec.flags.length > 0) {
lines.push("");
lines.push(header("flags:"));
const wFlag = Math.max(...spec.flags.map((f) => f.flag.length));
for (const f of spec.flags) {
lines.push(` ${f.flag.padEnd(wFlag)} ${f.purpose}`);
}
}
lines.push("");
lines.push("global flags + exit codes: `kestrel --help`");
process.stdout.write(lines.join("\n") + "\n");
return 0;
}