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
text/typescript
/**
* # 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 } : {}),
});
}