UNPKG

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
/** * # 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; }