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.

1,025 lines (1,018 loc) 42.6 kB
import { demoLessonObject, neverFiredLine, paidBoundaryNudgeObject, renderDemoLesson, renderPaidBoundaryNudge, renderSimMenu, renderSimStory, renderStandDownLesson, standDownLessonObject } from "./bin-awvz75fn.js"; import { appendHostedRun, defaultHostedStorePath } from "./bin-m9a64j9t.js"; import { DEFAULT_API, idemKey } from "./bin-qewrvqtv.js"; import { renderOffer, renderSettlementRequired } from "./bin-x2cbtyjz.js"; import { parseArgs } from "./bin-1gc4zavq.js"; import { CliError, EXIT, asConnectionError } from "./bin-t9ggwnv5.js"; import { canonicalPlansText } from "./bin-29be75ss.js"; import { sha256 } from "./bin-8pchjxn2.js"; import { KestrelParseError, VERIFY_KEYS_PATH, asOfferResponse, asSettlementAffordance, asSettlementResume, asWireOffer, parse, parseMarkdown } from "./bin-2ywrx58g.js"; // src/cli/commands/sim.ts import { readFileSync } from "node:fs"; // src/cli/backend/hosted.ts var PROOF_WEB_BASE = "https://kestrel.markets"; function slugifyTitle(title) { return title.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); } var SCENARIO_ALIASES = { "wsb-gme-meme-short-squeeze": "meme-stock-short-squeeze", "wsb-gme-short-squeeze": "meme-stock-short-squeeze", "covid-crash-march-2020": "pandemic-volatility-crash", "chip-earnings-blowout": "ai-chip-earnings-blowout", "intraday-opening-drive": "intraday-opening-drive-momentum" }; function resolveScenarioSlug(slug, entries) { const canonical = SCENARIO_ALIASES[slug] ?? slug; const viaAlias = canonical !== slug; for (const entry of entries) { if (slugifyTitle(entry.title) === canonical) { return { entry, canonicalSlug: canonical, requestedSlug: slug, viaAlias }; } } return null; } function catalogSlugs(entries) { return entries.map((e) => slugifyTitle(e.title)); } function nearMissSlugs(input, entries, limit = 3) { const inputTokens = new Set(input.split("-").filter(Boolean)); const scored = catalogSlugs(entries).map((slug) => { const shared = slug.split("-").filter((t) => inputTokens.has(t)).length; let prefix = 0; while (prefix < slug.length && prefix < input.length && slug[prefix] === input[prefix]) prefix += 1; return { slug, shared, prefix }; }); scored.sort((a, b) => b.shared - a.shared || b.prefix - a.prefix || (a.slug < b.slug ? -1 : 1)); return scored.slice(0, limit).map((s) => s.slug); } class HostedFunnel { base; fetch; constructor(opts) { this.base = opts.baseUrl.replace(/\/+$/, ""); const f = opts.fetch ?? globalThis.fetch; if (f === undefined) throw new CliError({ code: "RUNTIME_UNAVAILABLE", exit: EXIT.RUNTIME_UNAVAILABLE, message: "global fetch unavailable — need node ≥18 or bun for `kestrel sim`" }); this.fetch = f; } get verifyKeysUrl() { return `${this.base}${VERIFY_KEYS_PATH}`; } async catalog() { const res = await this.fetch(`${this.base}/catalog`, { headers: { accept: "application/json" } }); if (!res.ok) throw httpErr(res.status, "GET /catalog failed"); const body = await res.json(); const entries = Array.isArray(body) ? body : body?.entries; if (!Array.isArray(entries)) throw httpErr(502, "GET /catalog returned no entries[]"); return { entries: verifiedCatalogEntries(entries) }; } async mintTrial(opts = {}) { const headers = { "content-type": "application/json", accept: "application/json" }; if (opts.copyToken !== undefined && opts.copyToken !== "") headers["x-kestrel-copy-token"] = opts.copyToken; const res = await this.fetch(`${this.base}/capabilities/trial`, { method: "POST", headers, body: JSON.stringify({}) }); if (!res.ok) throw httpErr(res.status, "trial capability mint failed"); return verifiedHostedTrial(await safeJson(res)); } async verifyKeys(opts = {}) { const headers = { accept: "application/json" }; if (opts.copyToken !== undefined && opts.copyToken !== "") headers["x-kestrel-copy-token"] = opts.copyToken; const res = await this.fetch(`${this.base}${VERIFY_KEYS_PATH}`, { headers }); if (!res.ok) throw httpErr(res.status, `GET ${VERIFY_KEYS_PATH} failed`); return verifiedVerifyKeys(await safeJson(res)); } async publishedProof(id, opts = {}) { const headers = { accept: "application/json" }; if (opts.copyToken !== undefined && opts.copyToken !== "") headers["x-kestrel-copy-token"] = opts.copyToken; const res = await this.fetch(`${this.base}/proof/${encodeURIComponent(id)}`, { headers }); if (res.status === 404) return null; if (!res.ok) throw httpErr(res.status, `GET /proof/${id} failed`); return verifiedPublishedProof(await safeJson(res), id); } async evidenceBundle(id, opts = {}) { const headers = { accept: "application/json" }; if (opts.copyToken !== undefined && opts.copyToken !== "") headers["x-kestrel-copy-token"] = opts.copyToken; const res = await this.fetch(`${this.base}/proof/${encodeURIComponent(id)}/evidence`, { headers }); if (res.status === 404) return null; if (!res.ok) throw httpErr(res.status, `GET /proof/${id}/evidence failed`); return verifiedEvidenceBundle(await safeJson(res), id); } async simulate(req, bearer) { const body = { source: req.source, dataset: { artifact_id: req.artifactId }, ...req.spend !== undefined ? { spend: req.spend } : {} }; const encoded = JSON.stringify(body); const idempotencyKey = idemKey("POST", "/simulate", body); let pendingOperationId; for (let attempt = 0;attempt < 2; attempt += 1) { const res = await this.fetch(`${this.base}/simulate`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${bearer}`, "idempotency-key": idempotencyKey }, body: encoded }); if (res.status === 402) { if (pendingOperationId !== undefined) throw evidenceError("evidence-finalize replay crossed an unexpected payment boundary"); const offer = asOfferResponse(await safeJson(res)); if (offer === null) throw httpErr(402, "402 body was not a structured OfferResponse"); return { gated: true, payment: offer }; } if (!res.ok) throw await httpErrFrom(res, "POST /simulate failed"); const value = await safeJson(res); const settlement = settlementRequiredOperation(value); if (settlement !== null) { if (pendingOperationId !== undefined) throw evidenceError("evidence-finalize replay crossed an unexpected settlement boundary"); return { settlementRequired: settlement }; } const pending = evidencePendingOperation(value); if (pending !== null) { if (pendingOperationId !== undefined && pending.operation_id !== pendingOperationId) throw evidenceError("evidence-finalize replay changed Operation identity"); pendingOperationId = pending.operation_id; if (attempt === 1) throw evidenceError(`R2 evidence remained pending after one effect-once finalize-only retry: ${pending.evidence_status.detail}`); continue; } const op = verifiedHostedOperation(value); if (pendingOperationId !== undefined && op.operation_id !== pendingOperationId) throw evidenceError("evidence-finalize replay changed Operation identity"); return { gated: false, value: op }; } throw evidenceError("R2 evidence finalize retry exhausted"); } async proof(artifact, receipt, operationId, bearer) { const headers = { accept: "application/json" }; if (bearer !== undefined) headers["authorization"] = `Bearer ${bearer}`; const res = await this.fetch(`${this.base}/proof/${encodeURIComponent(artifact.artifact_id)}`, { headers }); if (!res.ok) throw httpErr(res.status, `GET /proof/${artifact.artifact_id} failed`); return verifiedHostedProof(await safeJson(res), artifact, receipt, operationId); } } function summarizeReceipts(receipts) { let meteredCount = 0; let budgetRemaining = null; for (const r of receipts) { if (r.kind === "metered") { meteredCount += 1; if (typeof r.budget_remaining === "number") budgetRemaining = r.budget_remaining; } } return { meteredCount, budgetRemaining }; } function neverFiredReasonOf(proof) { const reason = proof?.result.diagnostics?.never_fired_reason; return typeof reason === "string" && reason.trim().length > 0 ? reason : undefined; } function gradedMetrics(metrics) { const m = metrics ?? {}; return { orderCount: numOr0(m["order_count"]), fillCount: numOr0(m["fill_count"]), realizedPnl: numOr0(m["realized_pnl"]) }; } function gradeArtifactOf(op) { return op.artifacts.find((artifact) => artifact.kind === "grade"); } function gradeReceiptOf(op) { return op.receipts.find((receipt) => receipt.kind === "grade"); } function proofWebUrl(artifactId) { return `${PROOF_WEB_BASE}/proof/${artifactId}`; } function numOr0(v) { return typeof v === "number" && Number.isFinite(v) ? v : 0; } var ARTIFACT_ID_RE = /^art_[0-9a-f]{24}$/; var CONTENT_HASH_RE = /^sha256:[0-9a-f]{64}$/; var OPERATION_ID_RE = /^op_[A-Za-z0-9][A-Za-z0-9_-]*$/; function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function evidenceError(detail) { return httpErr(502, `POST /simulate did not return verified R2 evidence: ${detail}`); } function operationError(detail) { return httpErr(502, `POST /simulate returned no verified public Grade artifact: ${detail}`); } function catalogError(detail) { return httpErr(502, `GET /catalog returned invalid curated scenarios: ${detail}`); } function nonEmptyString(value) { return typeof value === "string" && value.trim().length > 0; } function verifiedCatalogEntries(values) { const entries = values.map((value, index) => { if (!isRecord(value)) throw catalogError(`entries[${index}] is not an object`); if (!nonEmptyString(value["artifact_id"])) throw catalogError(`entries[${index}].artifact_id is empty`); if (!nonEmptyString(value["title"])) throw catalogError(`entries[${index}].title is empty`); if (slugifyTitle(value["title"]).length === 0) throw catalogError(`entries[${index}].title does not produce a canonical slug`); if (!nonEmptyString(value["instrument"])) throw catalogError(`entries[${index}].instrument is empty`); const period = value["period"]; if (!isRecord(period) || !nonEmptyString(period["start"]) || !nonEmptyString(period["end"])) throw catalogError(`entries[${index}].period must name start and end`); if (typeof value["free"] !== "boolean") throw catalogError(`entries[${index}].free must be boolean`); if (typeof value["description"] !== "string") throw catalogError(`entries[${index}].description must be string`); if (!Number.isSafeInteger(value["frame_count"]) || value["frame_count"] <= 0) throw catalogError(`entries[${index}].frame_count must be a positive integer`); if (value["content_hash"] !== undefined && typeof value["content_hash"] !== "string") throw catalogError(`entries[${index}].content_hash must be string when present`); return value; }); const slugs = new Set; const artifacts = new Set; for (const entry of entries) { const slug = slugifyTitle(entry.title); if (slugs.has(slug)) throw catalogError(`duplicate catalog canonical slug ${JSON.stringify(slug)}`); if (artifacts.has(entry.artifact_id)) throw catalogError(`duplicate catalog artifact id ${JSON.stringify(entry.artifact_id)}`); slugs.add(slug); artifacts.add(entry.artifact_id); } return entries; } function trialError(detail) { return httpErr(502, `POST /capabilities/trial returned an unusable trial capability: ${detail}`); } var REQUIRED_TRIAL_SCOPES = ["sim", "grade"]; var FORBIDDEN_TRIAL_SCOPES = ["paper", "broker", "live"]; function verifiedHostedTrial(value) { if (!isRecord(value)) throw trialError("body is missing"); if (!nonEmptyString(value["capability"])) throw trialError("capability token is empty"); const scopes = value["scopes"]; if (scopes === undefined) return value; if (!Array.isArray(scopes) || !scopes.every((s) => typeof s === "string")) throw trialError("scopes must be an array of strings when advertised"); for (const required of REQUIRED_TRIAL_SCOPES) if (!scopes.includes(required)) throw trialError(`advertised scopes lack the required Simulation scope ${JSON.stringify(required)}`); for (const forbidden of FORBIDDEN_TRIAL_SCOPES) if (scopes.includes(forbidden)) throw trialError(`advertised scopes carry the forbidden authority scope ${JSON.stringify(forbidden)}`); return value; } function proofError(detail) { return httpErr(502, `GET /proof returned no verified Grade proof: ${detail}`); } function evidencePendingOperation(value) { if (!isRecord(value) || value["status"] !== "running" || !OPERATION_ID_RE.test(String(value["operation_id"] ?? ""))) return null; const status = value["evidence_status"]; if (!isRecord(status) || status["state"] !== "pending" || typeof status["detail"] !== "string") return null; if (!Array.isArray(value["artifacts"]) || value["artifacts"].length !== 0) return null; if (!Array.isArray(value["receipts"]) || value["evidence"] !== undefined) return null; if (value["offer"] !== null) return null; const resume = value["resume"]; if (!isRecord(resume) || resume["method"] !== "POST" || resume["path"] !== "/api/simulate" || resume["operation_id"] !== value["operation_id"] || typeof value["cursor"] !== "string" || resume["cursor"] !== value["cursor"] || resume["requires_settlement"] !== false || typeof resume["detail"] !== "string") return null; return value; } function settlementRequiredOperation(value) { if (!isRecord(value) || value["status"] !== "suspended") return null; const opId = value["operation_id"]; if (typeof opId !== "string" || !OPERATION_ID_RE.test(opId)) return null; const offer = asWireOffer(value["offer"]); if (offer === null || offer.operation_id !== opId) return null; const resume = asSettlementResume(value["resume"], opId); if (resume === null) return null; if (typeof value["cursor"] === "string" && resume.cursor !== value["cursor"]) return null; const settlement = asSettlementAffordance(value["settlement"]); return { operationId: opId, offer, resume, ...settlement !== null ? { settlement } : {} }; } function gradeArtifact(value) { if (!isRecord(value) || value["kind"] !== "grade") throw operationError("kind must be grade"); if (typeof value["artifact_id"] !== "string" || !ARTIFACT_ID_RE.test(value["artifact_id"])) throw operationError("artifact_id is malformed"); if (typeof value["content_hash"] !== "string" || !CONTENT_HASH_RE.test(value["content_hash"])) throw operationError("content_hash is malformed"); if (value["proof_url"] !== undefined && value["proof_url"] !== `/proof/${value["artifact_id"]}`) throw operationError("proof_url does not name the Grade artifact"); return value; } function gradeReceipt(value, artifact) { if (!isRecord(value) || value["kind"] !== "grade") throw operationError("Grade receipt kind must be grade"); if (value["root"] !== artifact.content_hash) throw operationError("Grade receipt root does not match the Grade artifact"); if (typeof value["signature"] !== "string" || value["signature"].length === 0) throw operationError("Grade receipt signature is missing"); return value; } function evidenceRef(value, expectedKind, field) { if (!isRecord(value)) throw evidenceError(`${field} is missing`); if (value["kind"] !== expectedKind) throw evidenceError(`${field}.kind must be ${expectedKind}`); if (typeof value["artifact_id"] !== "string" || !ARTIFACT_ID_RE.test(value["artifact_id"])) throw evidenceError(`${field}.artifact_id is malformed`); if (typeof value["content_hash"] !== "string" || !CONTENT_HASH_RE.test(value["content_hash"])) throw evidenceError(`${field}.content_hash is malformed`); if (!Number.isSafeInteger(value["byte_length"]) || value["byte_length"] <= 0) throw evidenceError(`${field}.byte_length must be a positive integer`); if (value["classification"] !== "private") throw evidenceError(`${field}.classification must be private`); return value; } function verifiedHostedOperation(value) { if (!isRecord(value) || !OPERATION_ID_RE.test(String(value["operation_id"] ?? ""))) throw httpErr(502, "POST /simulate returned malformed Operation id"); if (value["status"] !== "completed") throw evidenceError("operation is not completed"); if ("evidence_status" in value) throw evidenceError("legacy evidence_status is not durable proof"); const raw = value["evidence"]; if (!isRecord(raw)) throw evidenceError("bundle is missing"); const calls = raw["callRefs"]; if (!Array.isArray(calls)) throw evidenceError("callRefs must be an array"); if (calls.length !== 0) throw evidenceError(`inference-free sim unexpectedly captured ${calls.length} model call(s)`); const artifacts = value["artifacts"]; if (!Array.isArray(artifacts)) throw operationError("artifacts must be an array"); const grades = artifacts.filter((artifact) => isRecord(artifact) && artifact["kind"] === "grade"); if (grades.length !== 1) throw operationError(`expected exactly one Grade artifact, received ${grades.length}`); const grade = gradeArtifact(grades[0]); if (!Array.isArray(value["receipts"])) throw operationError("receipts must be an array"); const gradeReceipts = value["receipts"].filter((receipt) => isRecord(receipt) && receipt["kind"] === "grade"); if (gradeReceipts.length !== 1) throw operationError(`expected exactly one Grade receipt, received ${gradeReceipts.length}`); gradeReceipt(gradeReceipts[0], grade); const evidence = { busRef: evidenceRef(raw["busRef"], "session_bus", "busRef"), gradeRef: evidenceRef(raw["gradeRef"], "certified_grade", "gradeRef"), callRefs: calls.map((ref, index) => evidenceRef(ref, "agent_call", `callRefs[${index}]`)), manifestRef: evidenceRef(raw["manifestRef"], "manifest", "manifestRef") }; return { ...value, status: "completed", evidence }; } function verifiedHostedProof(value, artifact, receipt, operationId) { if (!isRecord(value)) throw proofError("body is missing"); if (value["proof_id"] !== artifact.artifact_id) throw proofError("proof_id does not match the Grade artifact"); if (value["kind"] !== "grade") throw proofError("kind must be grade"); if (value["root"] !== artifact.content_hash) throw proofError("root does not match the Grade artifact"); if (value["signature"] !== receipt.signature) throw proofError("signature does not match the Grade receipt"); const verification = value["verification"]; if (!isRecord(verification) || verification["verified"] !== true || verification["status"] !== "verified" || verification["algorithm"] !== "ed25519" || typeof verification["kid"] !== "string" || !Number.isSafeInteger(verification["epoch"])) throw proofError("signature did not re-verify"); const result = value["result"]; if (!isRecord(result) || result["subjectSessionId"] !== operationId) throw proofError("subjectSessionId does not match the Operation"); const metrics = result["metrics"]; if (!isRecord(metrics)) throw proofError("metrics are missing"); for (const name of ["order_count", "fill_count"]) { if (!Number.isSafeInteger(metrics[name]) || metrics[name] < 0) throw proofError(`metric ${name} must be a nonnegative integer`); } if (typeof metrics["realized_pnl"] !== "number" || !Number.isFinite(metrics["realized_pnl"])) throw proofError("metric realized_pnl is missing or non-finite"); return value; } function verifyKeysError(detail) { return httpErr(502, `GET ${VERIFY_KEYS_PATH} returned an unusable verify-set: ${detail}`); } function verifiedVerifyKeys(value) { const set = isRecord(value) ? value["grade_verify_keys"] : undefined; if (!Array.isArray(set)) throw verifyKeysError("grade_verify_keys is not an array"); const out = []; for (const k of set) { if (!isRecord(k)) continue; if (!nonEmptyString(k["kid"]) || k["algorithm"] !== "ed25519") continue; if (k["status"] !== "active" && k["status"] !== "accept-only") continue; const jwk = k["public_jwk"]; if (!isRecord(jwk) || jwk["kty"] !== "OKP" || jwk["crv"] !== "Ed25519" || !nonEmptyString(jwk["x"])) continue; if (!Number.isSafeInteger(k["epoch"])) continue; out.push({ kid: k["kid"], epoch: k["epoch"], algorithm: "ed25519", public_jwk: { kty: "OKP", crv: "Ed25519", x: jwk["x"] }, status: k["status"] }); } return out; } function publishedProofError(detail) { return httpErr(502, `GET /proof returned an unverifiable proof body: ${detail}`); } function asCertifiedCoveredFields(value) { if (!isRecord(value)) return; if (value["kind"] !== "grade") return; if (!isRecord(value["result"])) return; if (!nonEmptyString(value["evView"])) return; if (!isRecord(value["supportPartition"])) return; if (!nonEmptyString(value["pinnedDataRef"])) return; if (!nonEmptyString(value["pinnedFillModelVersion"])) return; if (!nonEmptyString(value["judge"])) return; if (!nonEmptyString(value["issuedAt"])) return; if (!Array.isArray(value["boundRoots"]) || !value["boundRoots"].every((r) => typeof r === "string")) return; if (!nonEmptyString(value["kid"])) return; if (!Number.isSafeInteger(value["epoch"])) return; if (typeof value["replayable"] !== "boolean") return; if (value["pinnedProvenanceRef"] !== undefined && !nonEmptyString(value["pinnedProvenanceRef"])) return; if (value["pinnedAuthorProvenanceRef"] !== undefined && !nonEmptyString(value["pinnedAuthorProvenanceRef"])) return; return { kind: "grade", result: value["result"], evView: value["evView"], supportPartition: value["supportPartition"], pinnedDataRef: value["pinnedDataRef"], ...value["pinnedProvenanceRef"] !== undefined ? { pinnedProvenanceRef: value["pinnedProvenanceRef"] } : {}, ...value["pinnedAuthorProvenanceRef"] !== undefined ? { pinnedAuthorProvenanceRef: value["pinnedAuthorProvenanceRef"] } : {}, pinnedFillModelVersion: value["pinnedFillModelVersion"], judge: value["judge"], issuedAt: value["issuedAt"], boundRoots: value["boundRoots"], ...value["counterfactual"] !== undefined ? { counterfactual: value["counterfactual"] } : {}, kid: value["kid"], epoch: value["epoch"], replayable: value["replayable"] }; } function verifiedPublishedProof(value, id) { if (!isRecord(value)) throw publishedProofError("body is missing"); if (value["proof_id"] !== undefined && value["proof_id"] !== id) throw publishedProofError("proof_id does not match the requested id"); if (!nonEmptyString(value["root"])) throw publishedProofError("root is missing"); if (!nonEmptyString(value["signature"])) throw publishedProofError("signature is missing"); const verification = value["verification"]; if (!isRecord(verification)) throw publishedProofError("verification block is missing"); if (!nonEmptyString(verification["kid"])) throw publishedProofError("verification.kid is missing"); if (!Number.isSafeInteger(verification["epoch"])) throw publishedProofError("verification.epoch is missing"); const coveredFields = asCertifiedCoveredFields(value["coveredFields"]); return { proof_id: id, root: value["root"], signature: value["signature"], kid: verification["kid"], epoch: verification["epoch"], verificationClaim: verification["verified"] === true, ...coveredFields !== undefined ? { coveredFields } : {} }; } function evidenceBundleError(detail) { return httpErr(502, `GET /proof/{id}/evidence returned an unusable evidence bundle: ${detail}`); } function asPublishedGrade(value) { if (!isRecord(value)) return; if (!nonEmptyString(value["subjectSessionId"])) return; const metrics = value["metrics"]; if (!isRecord(metrics)) return; for (const v of Object.values(metrics)) if (typeof v !== "number" || !Number.isFinite(v)) return; return { subjectSessionId: value["subjectSessionId"], metrics }; } function verifiedEvidenceBundle(value, id) { if (!isRecord(value)) throw evidenceBundleError("body is missing"); if (value["schema"] !== "kestrel.evidence-bundle/v1") throw evidenceBundleError(`schema must be "kestrel.evidence-bundle/v1"`); if (value["proof_id"] !== undefined && value["proof_id"] !== id) throw evidenceBundleError("proof_id does not match the requested id"); if (!nonEmptyString(value["bus"])) throw evidenceBundleError("bus (session-Bus JSONL text) is missing"); if (!nonEmptyString(value["blotter"])) throw evidenceBundleError("blotter (published Blotter bytes) is missing"); if (value["engine_version"] !== undefined && typeof value["engine_version"] !== "string") throw evidenceBundleError("engine_version must be a string when present"); if (value["fill_model"] !== undefined && typeof value["fill_model"] !== "string") throw evidenceBundleError("fill_model must be a string when present"); const publishedGrade = asPublishedGrade(value["publishedGrade"]); const coveredFields = asCertifiedCoveredFields(value["coveredFields"]); return { proof_id: id, bus: value["bus"], blotter: value["blotter"], ...typeof value["engine_version"] === "string" ? { engine_version: value["engine_version"] } : {}, ...typeof value["fill_model"] === "string" ? { fill_model: value["fill_model"] } : {}, ...publishedGrade !== undefined ? { publishedGrade } : {}, ...coveredFields !== undefined ? { coveredFields } : {}, ...nonEmptyString(value["root"]) ? { root: value["root"] } : {} }; } async function safeJson(res) { try { return await res.json(); } catch { return null; } } function httpErr(status, m) { const exit = status === 404 ? EXIT.NOT_FOUND : status >= 500 ? EXIT.RUNTIME_UNAVAILABLE : EXIT.GENERIC; return new CliError({ code: `HTTP_${status}`, exit, message: `${m} (HTTP ${status})` }); } var MAX_PROBLEM_FIELD = 2048; async function readProblem(res) { try { const body = await res.json(); if (body === null || typeof body !== "object") return; const b = body; const str = (v) => { if (typeof v !== "string" || v.length === 0) return; return v.length > MAX_PROBLEM_FIELD ? `${v.slice(0, MAX_PROBLEM_FIELD)}… [truncated]` : v; }; const title = str(b["title"]); const detail = str(b["detail"]); const code = str(b["code"]); const remediation = str(b["remediation"]) ?? str(b["remedy"]) ?? str(b["fix"]); if (title === undefined && detail === undefined && code === undefined && remediation === undefined) { return; } return { ...title !== undefined ? { title } : {}, ...detail !== undefined ? { detail } : {}, ...code !== undefined ? { code } : {}, ...remediation !== undefined ? { remediation } : {}, status: res.status }; } catch { return; } } async function httpErrFrom(res, m) { const problem = await readProblem(res); const base = httpErr(res.status, m); if (problem === undefined) return base; const parts = [problem.title, problem.detail].filter((s) => s !== undefined); const suffix = parts.length > 0 ? `: ${parts.join(" — ")}` : ""; return new CliError({ code: base.code, exit: base.exit, message: `${base.message}${suffix}`, ...problem.remediation !== undefined ? { hint: problem.remediation } : {}, problem }); } // src/cli/render/residency.ts var MCP_ENDPOINT = "https://mcp.kestrel.markets/mcp"; var PROVE_ONE_LINER = "npx kestrel.markets prove"; function residencyObject(proofUrl) { return { mcpServers: { "kestrel-markets": { type: "streamable-http", url: MCP_ENDPOINT } }, agentsMdLine: `- kestrel.markets: reproduce/extend this proof — ${proofUrl}`, cli: PROVE_ONE_LINER }; } function renderResidencyOneLine(proofUrl) { return `keep this proof URL — the shareable durable handle for this run; \`kestrel runs list\` also finds it from any directory (recorded to your home ledger ~/.kestrel, not just this folder): ${proofUrl}`; } function renderResidencySnippet(proofUrl) { const snippet = residencyObject(proofUrl); const mcpJson = JSON.stringify({ mcpServers: snippet.mcpServers }, null, 2); const lines = []; lines.push(""); lines.push("keep this — reproduce or extend this proof:"); lines.push(" 1) add the Kestrel MCP server to your agent config:"); for (const l of mcpJson.split(` `)) lines.push(` ${l}`); lines.push(" 2) add to your AGENTS.md:"); lines.push(` ${snippet.agentsMdLine}`); lines.push(" 3) or just run it again, no credential:"); lines.push(` ${snippet.cli}`); return lines.join(` `); } // src/cli/commands/sim.ts var STAND_DOWN_DOC = `# demo: stand-down baseline — supply --plans <file> to author a strategy `; var STAND_DOWN_EXAMPLE_PLAN = `PLAN starter budget 1R ttl +24h WHEN spot > 0 DO buy 1 shares @ spot`; function usageErr(message, hint) { return new CliError({ code: "USAGE", exit: EXIT.USAGE, message, ...hint !== undefined ? { hint } : {} }); } function classifySelector(token) { const hasUpper = /[A-Z]/.test(token); const hasLower = /[a-z]/.test(token); if (hasUpper && hasLower) return "mixed"; if (hasUpper) return "tickers"; return "slug"; } var DATE_RE = /^\d{4}-\d{2}(-\d{2})?$/; var RANGE_RE = /^\d{4}-\d{2}(-\d{2})?\.\.\d{4}-\d{2}(-\d{2})?$/; var REL_WHEN = new Set(["yesterday", "last-week", "last-month"]); function looksLikeWhen(token) { return DATE_RE.test(token) || RANGE_RE.test(token) || REL_WHEN.has(token); } function parseSimSelector(positionals) { const first = positionals[0]; if (positionals.length > 1) { const when = positionals[1]; throw usageErr(`date-window selectors are reserved — \`kestrel sim ${first} ${when}\` is not available yet`, "this PR ships curated scenario slugs only; ticker × <when> selectors are coming (kestrel-do4). Run `kestrel sim` for the menu."); } const kind = classifySelector(first); if (kind === "mixed") { throw usageErr(`"${first}" is ambiguous: a lowercase scenario slug or an UPPERCASE ticker selector? — mixed case is reserved`, "use an all-lowercase scenario slug (e.g. meme-stock-short-squeeze) or, once shipped, all-UPPERCASE tickers (e.g. SPY). Run `kestrel sim` for the menu."); } if (kind === "tickers") { throw usageErr(`ticker selectors are reserved — \`kestrel sim ${first} <when> --plans <file>\` is not available yet`, "this PR ships curated lowercase scenario slugs only; UPPERCASE ticker × <when> selectors are coming (kestrel-do4). Run `kestrel sim` for the menu."); } if (looksLikeWhen(first)) { throw usageErr(`"${first}" is a <when> with no scenario or ticker — reserved`, "name a scenario slug (kestrel sim <slug>); a standalone market window is coming (kestrel-do4). Run `kestrel sim` for the menu."); } return { scenarioSlug: first }; } function asParseError(e) { if (e instanceof KestrelParseError) return new CliError({ code: "PARSE", exit: EXIT.USAGE, message: e.message }); return usageErr(e instanceof Error ? e.message : String(e)); } function loadStrategy(plansPath, bundledDefault) { if (plansPath === undefined) { const doc = bundledDefault ?? { source: STAND_DOWN_DOC, label: "stand-down baseline (demo) — supply --plans <file> to author a strategy" }; const documents2 = [parse(doc.source)]; const source2 = canonicalPlansText(documents2); return { source: source2, label: doc.label, digest: sha256(source2) }; } let text; try { text = plansPath === "-" ? readFileSync(0, "utf8") : readFileSync(plansPath, "utf8"); } catch (e) { throw new CliError({ code: "NOT_FOUND", exit: EXIT.NOT_FOUND, message: `cannot read --plans ${JSON.stringify(plansPath)}: ${e instanceof Error ? e.message : String(e)}` }); } let documents; try { documents = plansPath.endsWith(".md") ? parseMarkdown(text) : [parse(text)]; } catch (e) { throw asParseError(e); } const source = canonicalPlansText(documents); return { source, label: plansPath, digest: sha256(source) }; } function resolveSimBase(globals, env) { const raw = globals.api ?? env.KESTREL_API; if (raw === undefined || raw === "" || raw === "default") return DEFAULT_API; return raw; } function resolveCopyToken(flags, env) { const raw = flags.get("copy-token") ?? env.KESTREL_COPY_TOKEN; return raw === undefined || raw === "" ? undefined : raw; } function resolveSpend(flags) { const raw = flags.get("budget"); if (raw === undefined) return; const budget = Number(raw); if (raw.trim() === "" || !Number.isFinite(budget) || budget < 0) throw usageErr(`--budget must be a finite, non-negative number (got ${JSON.stringify(raw)})`, "declare a spend ceiling in USD, e.g. `--budget 0.50`; use `--budget 0` to pause at the first metered draw"); return { budget }; } async function simCommand(argv, ctx, globals, deps = {}) { const { positionals, flags } = parseArgs(argv, new Set, new Set(["plans", "copy-token", "budget"])); const env = deps.env ?? process.env; const base = resolveSimBase(globals, env); const client = new HostedFunnel({ baseUrl: base, ...deps.fetch !== undefined ? { fetch: deps.fetch } : {} }); try { return await runSimFunnel(client, base, positionals, flags, ctx, deps, env); } catch (e) { throw asConnectionError(e, base) ?? e; } } async function runSimFunnel(client, base, positionals, flags, ctx, deps, env) { if (positionals.length === 0) { const { entries: entries2 } = await client.catalog(); return renderMenu(entries2, ctx); } const { scenarioSlug } = parseSimSelector(positionals); const { entries } = await client.catalog(); const resolved = resolveScenarioSlug(scenarioSlug, entries); if (resolved === 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" }); } const plansPath = flags.get("plans"); const strategy = loadStrategy(plansPath); const copyToken = resolveCopyToken(flags, env); const spend = resolveSpend(flags); return runHostedProof({ client, resolved, strategy, ctx, hostedStorePath: deps.hostedStorePath ?? defaultHostedStorePath(env), residencyOneLine: true, authoredPlan: plansPath !== undefined, ...plansPath === undefined ? { standDownLesson: { scenarioSlug: resolved.canonicalSlug, examplePlanSource: STAND_DOWN_EXAMPLE_PLAN, swapInCli: `npx kestrel.markets sim ${resolved.canonicalSlug} --plans <your-plan.kestrel>`, proveCli: "npx kestrel.markets prove" } } : {}, ...spend === undefined ? { paidBoundaryNudge: { scenarioSlug: resolved.canonicalSlug } } : {}, ...copyToken !== undefined ? { copyToken } : {}, ...spend !== undefined ? { spend } : {} }); } async function runHostedProof(run) { const { client, resolved, strategy, ctx } = run; const trial = await client.mintTrial(run.copyToken !== undefined ? { copyToken: run.copyToken } : {}); const g = await client.simulate({ source: strategy.source, artifactId: resolved.entry.artifact_id, ...run.spend !== undefined ? { spend: run.spend } : {} }, trial.capability); if ("settlementRequired" in g) { renderSettlementRequired(g.settlementRequired, ctx); return EXIT.PAYMENT_REQUIRED; } if (g.gated) { renderOffer(g.payment, ctx); return EXIT.PAYMENT_REQUIRED; } const op = g.value; const gradeArtifact2 = gradeArtifactOf(op); const gradeReceipt2 = gradeReceiptOf(op); const proofArtifactId = gradeArtifact2.artifact_id; const proofUrl = proofWebUrl(proofArtifactId); let proof = null; try { proof = await client.proof(gradeArtifact2, gradeReceipt2, op.operation_id, trial.capability); } catch (e) { if (!isProofSettling(e)) throw e; process.stderr.write(`warning: the grade proof for ${op.operation_id} is not yet readable (settling — retry shortly); ` + `metrics are pending and this run is not yet in \`kestrel runs list --hosted\`: ${e.message} `); } const metrics = proof === null ? null : gradedMetrics(proof.result.metrics); const neverFiredReason = metrics !== null && metrics.orderCount === 0 ? neverFiredReasonOf(proof) : undefined; if (proof !== null && metrics !== null) { run.onProof?.({ proofUrl, proofId: proofArtifactId, root: proof.root, signature: proof.signature, kid: proof.verification.kid, epoch: proof.verification.epoch, metrics }); } if (proof !== null && metrics !== null) { recordHostedRun(run.hostedStorePath, { source: "hosted", slug: resolved.canonicalSlug, requested_slug: resolved.requestedSlug, operation_id: op.operation_id, grade_artifact_id: proofArtifactId, bus_artifact_id: op.evidence.busRef.artifact_id, manifest_artifact_id: op.evidence.manifestRef.artifact_id, order_count: metrics.orderCount, fill_count: metrics.fillCount, realized_pnl: metrics.realizedPnl, proof_url: proofUrl, issued_at: proof.issuedAt ?? "", strategy: strategy.label, strategy_digest: strategy.digest, ...run.copyToken !== undefined ? { copy_token: run.copyToken } : {}, ...neverFiredReason !== undefined ? { never_fired_reason: neverFiredReason } : {} }); } if (!run.quiet && run.copyToken !== undefined && run.authoredPlan !== true) { process.stderr.write(`notice: --copy-token continues a proof, but the origin plan is NOT recoverable from the opaque token — ` + `this run used the bundled/stand-down baseline (${strategy.label}), not the strategy you are continuing. ` + `Supply \`--plans <your-plan>\` to author the continuation; the lineage back to the origin is recorded on the receipt. `); } if (run.quiet) return 0; if (ctx.mode === "json") { process.stdout.write(JSON.stringify({ schema: "kestrel.sim/v1", scenario: resolved.canonicalSlug, requested: resolved.requestedSlug, operation: op, metrics: proof === null ? null : proof.result.metrics, ...neverFiredReason !== undefined ? { never_fired_reason: neverFiredReason } : {}, proof_url: proofUrl, ...proof === null ? { proof_settling: true } : {}, ...run.emitResidency ? { residency: residencyObject(proofUrl) } : {}, ...run.residencyOneLine && !run.emitResidency ? { residency_guidance: renderResidencyOneLine(proofUrl) } : {}, ...run.demoLesson !== undefined ? { demo: demoLessonObject(run.demoLesson) } : {}, ...run.standDownLesson !== undefined && metrics !== null && metrics.orderCount === 0 ? { stand_down: standDownLessonObject(run.standDownLesson) } : {}, ...run.paidBoundaryNudge !== undefined && metrics !== null ? { paid_nudge: paidBoundaryNudgeObject(run.paidBoundaryNudge) } : {} }) + ` `); return 0; } const story = { canonicalSlug: resolved.canonicalSlug, requestedSlug: resolved.requestedSlug, viaAlias: resolved.viaAlias, title: resolved.entry.title, instrument: resolved.entry.instrument, period: resolved.entry.period, frameCount: resolved.entry.frame_count, strategyLabel: strategy.label, operation: { id: op.operation_id, status: op.status }, receipts: summarizeReceipts(op.receipts), evidence: op.evidence, metrics, proofUrl }; renderSimStory(story, ctx); if (neverFiredReason !== undefined) process.stdout.write(neverFiredLine(neverFiredReason) + ` `); if (run.demoLesson !== undefined) process.stdout.write(renderDemoLesson(run.demoLesson) + ` `); if (run.standDownLesson !== undefined && metrics !== null && metrics.orderCount === 0) process.stdout.write(renderStandDownLesson(run.standDownLesson) + ` `); if (run.paidBoundaryNudge !== undefined && metrics !== null) process.stdout.write(renderPaidBoundaryNudge(run.paidBoundaryNudge) + ` `); if (run.emitResidency) process.stdout.write(renderResidencySnippet(proofUrl) + ` `); if (run.residencyOneLine && !run.emitResidency) process.stdout.write(renderResidencyOneLine(proofUrl) + ` `); return 0; } function isProofSettling(e) { return e instanceof CliError && e.code === "HTTP_404"; } function recordHostedRun(storePath, row) { try { appendHostedRun(storePath, row); } catch (e) { process.stderr.write(`warning: could not append the hosted-run receipt to ${JSON.stringify(storePath)} — ` + `\`kestrel runs list --hosted\` will not show this run: ${e instanceof Error ? e.message : String(e)} `); } } function renderMenu(entries, ctx) { const scenarios = entries.map((e) => ({ slug: slugifyTitle(e.title), title: e.title, instrument: e.instrument, artifactId: e.artifact_id, free: e.free })); const menu = { scenarios, aliases: SCENARIO_ALIASES }; if (ctx.mode === "json") { process.stdout.write(JSON.stringify({ schema: "kestrel.sim.menu/v1", scenarios: scenarios.map((s) => ({ slug: s.slug, title: s.title, instrument: s.instrument, artifact_id: s.artifactId, free: s.free })), aliases: SCENARIO_ALIASES, usage: "kestrel sim <scenario-slug> [--plans <file>]" }) + ` `); return 0; } renderSimMenu(menu, ctx); return 0; } export { slugifyTitle, resolveScenarioSlug, nearMissSlugs, HostedFunnel, STAND_DOWN_DOC, STAND_DOWN_EXAMPLE_PLAN, classifySelector, parseSimSelector, loadStrategy, resolveSimBase, resolveCopyToken, resolveSpend, simCommand, runHostedProof };