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.

194 lines (179 loc) 10 kB
/** * # cli/commands/prove — `prove` (LIGHT, node-runnable): the zero-credential front door (n04e.1). * * `npx kestrel.markets prove` with NO args, NO key, NO config, and NO prompt must SUCCEED: it * selects a pinned default free scenario, runs the same hosted mint → simulate → proof flow the * `sim` verb drives (against the labeled stand-down baseline, author-no-strategy), prints the * graded story + the shareable proof URL, and — this is its only behavioral delta vs `sim` — * exhausts to the machine-parseable RESIDENCY snippet addressed to the agent that just ran it. * * - bare `prove` → the pinned default scenario (never a menu, never a prompt). * - `prove <lowercase-slug>` → that curated scenario. * - `prove --plans <file>` → author a real strategy for the run. * * LIGHT by construction: it reuses `sim.ts`'s hosted core over fetch only (no bun/chdb), so the * front-door snippet runs under plain node via npx. It always routes to the hosted funnel (a * proof NAMES platform data), the default base overridable by `--api`/`KESTREL_API`. * * BOUNDARY: like `sim`, `prove` is a THIN HTTP client of the public `api.kestrel.markets` — it * embeds no platform business logic. The opaque `--copy-token` it forwards is an uninterpreted * pass-through nonce (see {@link ../backend/hosted.ts HostedFunnel.mintTrial}); all attribution * meaning lives in the managed platform. */ import { HostedFunnel, resolveScenarioSlug, nearMissSlugs, slugifyTitle, type HostedCatalogEntry, type ResolvedScenario } from "../backend/hosted.ts"; import { defaultHostedStorePath } from "../../ledger/hosted.ts"; import type { OutputCtx, GlobalFlags } from "../context.ts"; import { parseArgs } from "../args.ts"; import { CliError, EXIT } from "../errors.ts"; import { resolveSimBase, loadStrategy, resolveCopyToken, parseSimSelector, runHostedProof, } from "./sim.ts"; type FetchLike = (url: string, init?: RequestInit) => Promise<Response>; interface Env { readonly [k: string]: string | undefined; } /** * The pinned default scenario a bare `prove` runs. A stable, all-free catalog slug so the * zero-credential front door is deterministic across machines (site-is-spec). If the live * catalog no longer carries it, {@link selectDefaultScenario} falls back to the first free * entry — and fails CLOSED (never a menu) if the catalog offers none. * * MUST be a tape the bundled sampler starter actually FILLS on (kestrel-etvn): the starter * rests a limit that fills only when price reverts back through the entry. The mean-reversion * range-fade tape reverts through the open, so the resting limit crosses and the run certifies * a real, filled trade (order_count=1 fill_count=1 realized_pnl=-2 — an honest small loss). * An UP-ONLY tape (the prior `meme-stock-short-squeeze` default) never fills, which is how the * bare front door came to certify an empty session. Aligned with the platform quickstart * (kestrel.markets PR #328) — same RNGE tape, same starter shape, same graded result. */ export const DEFAULT_PROVE_SLUG = "mean-reversion-range-fade"; /** * The BUNDLED sampler-starter plan a bare `prove` runs when the caller supplies no `--plans`. * Deliberately minimal — buy one share at the open and hold to the risk-envelope `ttl` — and * clearly a REPLACEABLE EXAMPLE (never a hidden strategy): it is exactly parallel to what * `--plans` uploads, so author-provenance stays customer/explicitly-supplied server-side. * On the range-fade tape ({@link DEFAULT_PROVE_SLUG}) this resting limit fills, so the bare * front door certifies a real trade instead of an empty session. Same three lines the platform * quickstart authors (kestrel.markets PR #328). Never fabricated: the grade is the honest, * filled result of THIS plan on THIS tape. */ export const SAMPLER_STARTER_DOC = "PLAN starter budget 1R ttl +24h\n WHEN spot > 0\n DO buy 1 shares @ spot"; /** The self-describing label for the bundled sampler starter, printed as the strategy label. */ export const SAMPLER_STARTER_LABEL = "sampler starter (bundled example — supply --plans <file> to author your own)"; /** The swap-in re-entry the one-frame lesson points a reader at: author your OWN plan. */ const SWAP_IN_CLI = "npx kestrel.markets prove --plans <your-plan.kestrel>"; /** * Select the scenario a BARE `prove` runs — fail-closed. Bare prove must NEVER fall through to * a menu or a prompt (that is the inert-front-door failure), so an empty selection is a loud * refusal, never a silent default. Preference: the pinned {@link DEFAULT_PROVE_SLUG}, else the * first `free:true` catalog entry. */ export function selectDefaultScenario(entries: readonly HostedCatalogEntry[]): ResolvedScenario { // The front door is FREE by construction — the pinned default is used only when it is a free // catalog entry; otherwise fall through to the first free one (never a paid default that would // dead-end at a 402). const pinned = resolveScenarioSlug(DEFAULT_PROVE_SLUG, entries); if (pinned !== null && pinned.entry.free) return pinned; const firstFree = entries.find((e) => e.free); if (firstFree === undefined) throw new CliError({ code: "NOT_FOUND", exit: EXIT.NOT_FOUND, message: "no free scenario is available for `prove` — the hosted catalog offered none", hint: "run `kestrel sim` to see the catalog, or name a scenario: `kestrel prove <slug>`", }); const slug = slugifyTitle(firstFree.title); return { entry: firstFree, canonicalSlug: slug, requestedSlug: slug, viaAlias: false }; } export interface ProveDeps { /** Injected fetch (tests pass an in-process funnel face); defaults to global fetch. */ readonly fetch?: FetchLike; /** Injected env (tests); defaults to process.env. */ readonly env?: Env; /** Where the hosted-run receipt is appended; defaults to the identity-bound home store * `~/.kestrel/hosted-runs.jsonl` ({@link defaultHostedStorePath}, kestrel-fq78). */ readonly hostedStorePath?: string; /** * Override the bare-scenario selector (tests only). The wiring guard: a stub that returns * `undefined` must make bare `prove` go RED (a loud refusal), NOT fall through to a menu. */ readonly selectDefault?: (entries: readonly HostedCatalogEntry[]) => ResolvedScenario | undefined; } /** * `prove` — the zero-credential front door. Bare = the pinned default scenario + the residency * exhaust; a slug runs that scenario; `--plans` authors a strategy; `--no-residency` suppresses * the snippet; `--copy-token`/`KESTREL_COPY_TOKEN` forwards an opaque nonce to the mint. */ export async function proveCommand( argv: readonly string[], ctx: OutputCtx, globals: GlobalFlags, deps: ProveDeps = {}, ): Promise<number> { const { positionals, flags, bools } = parseArgs(argv, new Set(["no-residency"]), new Set(["plans", "copy-token"])); const env = deps.env ?? (process.env as Env); const base = resolveSimBase(globals, env); const client = new HostedFunnel({ baseUrl: base, ...(deps.fetch !== undefined ? { fetch: deps.fetch } : {}) }); const { entries } = await client.catalog(); let resolved: ResolvedScenario; if (positionals.length === 0) { // Bare `prove` → the pinned default. FAIL CLOSED: never a menu, never a prompt. const select = deps.selectDefault ?? selectDefaultScenario; const picked = select(entries); if (picked === undefined) throw new CliError({ code: "NOT_FOUND", exit: EXIT.NOT_FOUND, message: "`prove` could not select a default scenario — the front door refuses to fall through to a menu", hint: "name a scenario: `kestrel prove <slug>`, or run `kestrel sim` for the catalog", }); resolved = picked; } else { // `prove <slug>` — the same case-rule as `sim` (reserved ticker/mixed/<when> forms refuse). const { scenarioSlug } = parseSimSelector(positionals); const match = resolveScenarioSlug(scenarioSlug, entries); if (match === null) { const near = nearMissSlugs(scenarioSlug, entries); throw new CliError({ code: "NOT_FOUND", exit: EXIT.NOT_FOUND, message: `unknown scenario slug ${JSON.stringify(scenarioSlug)}`, hint: near.length > 0 ? `did you mean: ${near.join(", ")}? — run \`kestrel sim\` for the full menu` : "run `kestrel sim` for the menu", }); } resolved = match; } // Bare `prove` (no `--plans`) defaults to the BUNDLED sampler starter — a labeled, replaceable // example that actually ARMS an order, so the front door certifies a real trade rather than an // empty session. An author-supplied `--plans` always wins (the bundled default is consulted only // when `plansPath` is undefined), so the customer's own strategy path is unchanged. const plansPath = flags.get("plans"); const usingBundledStarter = plansPath === undefined; const strategy = loadStrategy(plansPath, { source: SAMPLER_STARTER_DOC, label: SAMPLER_STARTER_LABEL }); const copyToken = resolveCopyToken(flags, env); return runHostedProof({ client, resolved, strategy, ctx, hostedStorePath: deps.hostedStorePath ?? defaultHostedStorePath(env), emitResidency: !bools.has("no-residency"), // Whether the caller authored a plan (`--plans`) vs the bundled sampler starter — governs the // `--copy-token` continuation notice (kestrel-gu9h): a copy-token run that did NOT author its own // plan cannot recover the origin plan from the opaque token, so it says so loudly instead of // silently running the bundled example. authoredPlan: !usingBundledStarter, // The one-frame lesson rides ONLY the bundled-starter path — never when the caller authored a plan. ...(usingBundledStarter ? { demoLesson: { planLabel: SAMPLER_STARTER_LABEL, planSource: strategy.source, swapInCli: SWAP_IN_CLI } } : {}), ...(copyToken !== undefined ? { copyToken } : {}), }); }