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.

784 lines (706 loc) 44 kB
/** * kestrel.markets/protocol — the OVERSIGHT contract (protocol v0.3). * * The cross-repo seam between the CLI (OSS `kestrel` — `ascii`/Ink Rendering) and * the Web dashboard (platform `kestrel.markets` — `html` Rendering). ADR-0035: the * human never places a ticket — the human mode is PM/Pod OVERSIGHT (positions, * plans, P&L, risk envelope) plus chat. "The agent's ASCII screen and the human's * HTML chart are two Renderings of one Frame" (CONTEXT.md, *Rendering*) — so if the * Web dashboard can show a number the CLI cannot, that number was fetched OUTSIDE * this contract, and the protocol is broken. This module owns the shared model; * both Renderings consume it, neither invents a value. * * Normative source: docs/design/oversight-protocol-contract.md §3–§5 (owner-approved * 2026-07-14). The model has exactly EIGHT parts and nothing else belongs in it. * * HARD CONSTRAINTS (identical to src/protocol/index.ts — see its header): * - ZERO RUNTIME dependencies. The ONLY imports are intra-protocol type-imports * (`./session.ts`, `./index.ts` — both dependency-free); this module pulls in NO * engine / session / blotter / fill / lang / grade / frame / render / ledger / * cli code and must typecheck with `chdb` uninstalled. The import-graph boundary * is an automated CI invariant (tests/protocol.boundary.test.ts). Hence the * acting Kernel is a STRUCTURAL MIRROR of `src/frame/types.ts` `Kernel` — never * an import of it (§3.2). CONFORMANCE, HONESTLY: today this leaf is a * HAND-MAINTAINED parallel of `src/frame/types.ts` with NO compiler link — its * field-for-field correspondence is verified by audit, not yet by a witness. The * total projector `src/oversight/project.ts` (bead kestrel-telx.2, NOT YET * LANDED) is the PLANNED conformance point: it will carry a `Record<keyof Kernel, * true>` key-witness on the `src/frame` side so that a section added to `Kernel` * breaks the build until this mirror handles it. Until it lands, adding a field * to `src/frame/types.ts` `Kernel*` and forgetting it here does NOT auto-red — so * any `Kernel*` edit must be reconciled against this leaf by hand. * - SHAPES ONLY. No signing keys, no Stripe/commerce logic. Every signed, * replayable artifact carries an opaque `*Ref` HANDLE, never a key or a signing * routine (`OversightAct` 'fund' carries `authorizationRef`, never a credential). * - GENERIC INSTRUMENTS ONLY. No founding-app tickers or strategy names in types, * comments, or examples. Illustrative fixtures use SPX/SPY/QQQ (ARCHITECTURE §7); * product symbols ride INSIDE opaque `symbol` fields at runtime. * * Version: `major.minor`. This leaf first landed at PROTOCOL_VERSION 0.3 (bumped 0.2 → 0.3, purely * additive). It now bumps 0.3 → 0.4 for the `ActReceipt` `state` field (kestrel-ysqj): `submitAct`'s * receipt gains an explicit queued-vs-bound stamp so 'admitted' and 'in force' are distinguishable to a * remote caller — a shape change to an existing type, hence a minor bump (index.ts owns the constant; * this note records why it moved). The schema tag `kestrel.session/<PROTOCOL_VERSION>` and the * content-addressed view/rendering identities track it; the graded-bus conformance root does NOT (it is * decoupled, ENGINE_VERSION-gated), so only the catalog-records bake re-pins, never a conformance root. * * NAMING: index.ts re-exports leaves with `export *` and already exports * `Position`/`Instrument`/`Order`/`Fill`/`Side`, so every oversight type here is * PREFIXED (`KernelPosition`, `KernelRestingOrder`, `KernelFill`, …). A collision is * a compile error, not a style question. * * CLOSED-VOCABULARY IDIOM (mandatory): every union ships its runtime tuple + the * both-ways exhaustiveness guard (`SESSION_DIAGNOSTICS` / `_SessionDiagnosticsExhaustive` * in session.ts), so a member added to (or removed from) a union without editing its * tuple BREAKS THE BUILD. That is what makes "an event outside the enum is a protocol * violation" enforceable rather than aspirational. */ import type { OperationId, SessionDiagnostic, SessionId, TurnEntry } from "./session.ts"; import type { Scope } from "./index.ts"; /* ── 1. Caller / session identity (ADR-0035 §a, §b) ─────────────────────────── */ export type CallerKind = "agent" | "human"; export const CALLER_KINDS = ["agent", "human"] as const satisfies readonly CallerKind[]; type _CallerKindsExhaustive<_Missing extends never = Exclude<CallerKind, (typeof CALLER_KINDS)[number]>> = true; /** How the Caller was decided. Auditable — never a guess we hide. */ export type CallerDetection = "flag" | "env" | "ci" | "tty"; export const CALLER_DETECTIONS = ["flag", "env", "ci", "tty"] as const satisfies readonly CallerDetection[]; type _CallerDetectionsExhaustive<_Missing extends never = Exclude<CallerDetection, (typeof CALLER_DETECTIONS)[number]>> = true; /** * Resolved ONCE, env-first / TTY-second (an agent can hold a PTY). The SINGLE source read by both * the render-mode resolver and platform-bound telemetry, so rendering and analytics can never * disagree about who is calling (ADR-0035 §a). * * FAIL-CLOSED: under ANY ambiguity `interactive` is `false`. A wrongly-launched TUI hangs an agent * forever — the worst failure; guessing "agent" wrong merely gives a human plain text. */ export interface Caller { readonly kind: CallerKind; /** The detected harness, when known (a coding-agent CLI, CI). `null` ⇒ unknown, never invented. */ readonly harness: string | null; readonly detectedBy: CallerDetection; /** Interactive session eligible: a CONFIDENT human only — both TTYs, no agent env, no CI. */ readonly interactive: boolean; } /** What an order MEANS when it fires (CONTEXT: Mode). Orthogonal to Caller and to authentication. */ export type Mode = "sim" | "paper" | "live"; export const MODES = ["sim", "paper", "live"] as const satisfies readonly Mode[]; type _ModesExhaustive<_Missing extends never = Exclude<Mode, (typeof MODES)[number]>> = true; /** * Who is watching what, under which authority. The deterministic `sessionId` (a genesis hash — the * same on a self-hosted Kestrel and on the managed backend) and the control-plane `operation` * (platform-minted, NOT content-derived) are BRANDED distinctly in `./session.ts` and are never * substituted for one another. With no account, a session opens on a local paper Pod: `mode:"paper"`, * `operation` absent (ADR-0035 §b) — no broker, no signup. */ export interface OversightIdentity { readonly caller: Caller; readonly sessionId: SessionId; readonly operation?: OperationId; readonly mode: Mode; readonly scopes: readonly Scope[]; } /* ── 2. The acting Kernel (structural mirror of src/frame/types.ts `Kernel`) ── */ // The protocol imports NOTHING, so this is a structural mirror, not an import. It is a // HAND-MAINTAINED parallel today: there is NO compiler link between these shapes and // `src/frame/types.ts`, so a field added there does NOT auto-red here. The PLANNED conformance // point is the total projector `src/oversight/project.ts` (bead kestrel-telx.2, not yet landed), // whose `Record<keyof Kernel, true>` key-witness will break the build when `Kernel` grows a // section. Until it lands, keep this mirror faithful BY HAND — audit every `Kernel*` shape below // field-for-field against its `src/frame/types.ts` counterpart, and record any DELIBERATE // exclusion at the type (see `KernelFill` / `RiskEnvelope` below) so a drop is never silent. export type Attribution = "OBS" | "CALC" | "MODEL"; export const ATTRIBUTIONS = ["OBS", "CALC", "MODEL"] as const satisfies readonly Attribution[]; type _AttributionsExhaustive<_Missing extends never = Exclude<Attribution, (typeof ATTRIBUTIONS)[number]>> = true; /** Every rendered number is a Field, carried with its provenance so the Rendering invents nothing. * `asOfSeq` is an ORDINAL, never a wall clock. */ export interface KernelField<T = number> { readonly value: T; readonly attribution: Attribution; /** Required for MODEL, absent for OBS/CALC. */ readonly source?: string; readonly modelVer?: string; readonly confidence?: number; readonly asOfSeq?: number; } /** A DATE-BLIND leg. `strike`/`right` are BOTH present for an option and BOTH absent for a * spot/equity leg (ADR-0017 — a fictional strike is never written). `symbol` is opaque: no * application ticker ever appears in this contract. */ export interface KernelLeg { readonly symbol: string; readonly strike?: number; readonly right?: "C" | "P"; } export interface KernelPosition { readonly leg: KernelLeg; readonly qty: number; // signed readonly basis: number; // average cost per contract readonly fair: number | null; // null ⇒ UNKNOWN, rendered as `—`, never blank /** Running UNREALIZED P&L in DOLLARS (mirror of `src/frame/types.ts` `Position.unrealUsd`, * kestrel-c11): `qty × (mark − basis) × multiplier`, marked to spot (equity/spot) or intrinsic * (option) with the SAME dollar scaling the fill engine's `pnl` applies. It rides the contract so * the human READS its P&L (`-$25.97`) instead of re-deriving it and mis-scaling cents-for-dollars * (the 100× abandon bug) — a Rendering invents no value (CONTEXT.md *Rendering*). `null` ⇒ the * mark is UNKNOWN (no spot) ⇒ rendered `—`, never a fabricated `0`. Required here (absent-not- * hidden): the projector emits explicit `null`, never a dropped key. */ readonly unrealUsd: number | null; /** The plan that OPENED the leg (provenance). */ readonly plan: string | null; /** The plan that OWNS this inventory claim — exactly one plan may claim a line; owner and engine * see the same claim (mutual visibility). Distinct from `plan`. */ readonly claimOwner: string | null; readonly structure: string | null; // e.g. a vertical, a barbell } /** `live` and `clamped` are INDEPENDENT flags: a working order whose price was clamped is BOTH. * They are never collapsed into one label — a `live` badge may never mask a clamped price * (RUNTIME §4: a silent price is forbidden). */ export interface KernelRestingOrder { readonly ref: string; readonly leg: KernelLeg; readonly side: "buy" | "sell"; readonly qty: number; readonly px: number; readonly live: boolean; readonly clamped: boolean; /** Price-resolution annotation (e.g. `fair=fallback(mid)`, `cap fair,0.73`). */ readonly note: string | null; readonly plan: string | null; } /** A fill since the last vantage. DELIBERATE MIRROR EXCLUSION vs `src/frame/types.ts` `FillRecord`: * the frame's `clock` (the HH:MM ET the fill printed at) is DROPPED. The oversight contract permits * a wall clock in EXACTLY ONE place — `SpectatorFrame.asof` (§4, invariant 4) — and fill ORDER is * carried by the Bus `seq`, never by a clock token on the deterministic path. Excluded on purpose, * recorded here so the drop is not silent (not an oversight of the mirror). */ export interface KernelFill { readonly leg: KernelLeg; readonly side: "buy" | "sell"; readonly qty: number; readonly px: number; readonly plan: string | null; } export type PlanLifecycle = "authored" | "armed" | "fired" | "managing" | "done"; export const PLAN_LIFECYCLES = [ "authored", "armed", "fired", "managing", "done", ] as const satisfies readonly PlanLifecycle[]; type _PlanLifecyclesExhaustive<_Missing extends never = Exclude<PlanLifecycle, (typeof PLAN_LIFECYCLES)[number]>> = true; export type PlanOutcome = "filled" | "expired" | "invalidated"; export const PLAN_OUTCOMES = ["filled", "expired", "invalidated"] as const satisfies readonly PlanOutcome[]; type _PlanOutcomesExhaustive<_Missing extends never = Exclude<PlanOutcome, (typeof PLAN_OUTCOMES)[number]>> = true; export interface KernelPlanState { readonly name: string; readonly state: PlanLifecycle; readonly outcome: PlanOutcome | null; /** A logged reason (a de-arm/invalidation reason, a wake note). Never blank-on-unknown. */ readonly note: string | null; /** The ARM-TIME gate-block reason when this plan is stuck `authored` on an unsatisfiable regime * gate (mirror of `src/frame/types.ts` `PlanStateEntry.blockedReason`, kestrel-50w). Present ⇒ * render `authored (blocked: <reason>)`, so a bare `authored` (a live plan awaiting its WHEN) is * DISTINGUISHABLE from one that can NEVER arm — the phantom-position trap (an overseer misreads * `authored` as armed-and-live and believes in a position that never existed). `null` ⇒ not * blocked. Fail-closed distinguishability, required here (absent-not-hidden). */ readonly blockedReason: string | null; } /** The cockpit risk envelope: remaining-R plus the three nested envelopes the fire-time router * lives inside — **plan ⊆ book ⊆ owner**. The OWNER envelope IS the funded balance (ADR-0035 §g): * you cannot lose more than you funded. * * DELIBERATE MIRROR EXCLUSION vs `src/frame/types.ts` `BudgetEnvelope`: the frame's `sizing` * (`SizingHeadroom` — the max fillable size the remaining-R budget admits, kestrel-m9i.32) is * DROPPED. It is an AGENT-authoring aid (so a model sizes WITHIN the envelope rather than hitting a * silent fire-time clamp); a PM never authors tickets (ADR-0035, CONTEXT.md *PM*), so it is not PM * oversight state. The bounded-risk fact the cockpit renders is the funded `ownerEnvelope`, carried * below. Excluded on purpose, recorded here so the drop is not silent. */ export interface RiskEnvelope { readonly remainingR: number; readonly planEnvelope: number; readonly bookEnvelope: number; readonly ownerEnvelope: number; } /** The plan-lifecycle usage view (distinct from the cockpit `RiskEnvelope`). */ export interface KernelBudget { readonly used: number | null; readonly remaining: number | null; readonly total: number | null; readonly maxConcurrentR: number | null; } export type WakeSeverity = "routine" | "elevated" | "urgent"; export const WAKE_SEVERITIES = ["routine", "elevated", "urgent"] as const satisfies readonly WakeSeverity[]; type _WakeSeveritiesExhaustive<_Missing extends never = Exclude<WakeSeverity, (typeof WAKE_SEVERITIES)[number]>> = true; /** Why the author is looking now + the RELATIVE deadline. `deadlineMin` is minutes-to-close — * never an absolute time, never a date (determinism). `null` ⇒ `T-— to close`. */ export interface KernelWake { readonly reason: string; readonly severity: WakeSeverity; readonly deadlineMin: number | null; } /** Per-vehicle book health (the routing gate). A `dark` vehicle is NAMED, never hidden, and taints * its dependents (they render UNKNOWN). */ export interface KernelVehicleHealth { readonly symbol: string; readonly bidPresentRate: number; // 0..1 — the market-maker-pull fingerprint readonly twoSided: boolean; readonly staleS: number; readonly dark: boolean; } export type EngineActionKind = "fired" | "cancelled" | "rejected" | "clamped"; export const ENGINE_ACTION_KINDS = [ "fired", "cancelled", "rejected", "clamped", ] as const satisfies readonly EngineActionKind[]; type _EngineActionKindsExhaustive<_Missing extends never = Exclude<EngineActionKind, (typeof ENGINE_ACTION_KINDS)[number]>> = true; /** One L0/L1 engine action since the last vantage. `asofSeq` is an ordinal, never a wall clock. * `reason` carries WHY a `rejected` action was refused (e.g. "uncovered sell refused: never * naked") — the fail-closed refusal is DATA the overseer can read, not a swallowed error. */ export interface KernelEngineAction { readonly id: string; readonly kind: EngineActionKind; readonly asofSeq: number; readonly reason: string | null; } /** An owner act as the kernel already records it (id + kind + ordinal). The TYPED act vocabulary * that PRODUCES these is `OversightAct` (§3.6); this is the recorded projection. */ export interface KernelOwnerAct { readonly id: string; readonly kind: string; readonly asofSeq: number; } export type ClaimType = "predictor" | "regime"; export const CLAIM_TYPES = ["predictor", "regime"] as const satisfies readonly ClaimType[]; type _ClaimTypesExhaustive<_Missing extends never = Exclude<ClaimType, (typeof CLAIM_TYPES)[number]>> = true; /** A predictor/regime claim MUST be an honest MODEL Field (source + modelVer + confidence). An * OBS/CALC "claim" is REFUSED at construction — a dishonest claim never renders as if honest. */ export interface KernelClaim { readonly field: KernelField; readonly claimType: ClaimType; } /** HARD, machine-checkable, narrowing-only — the ONLY channel that feeds admission (ADR-0026). */ export interface KernelMandate { readonly objective: string; readonly rUsd: number; // 1R = $X — the R-definition the grade uses readonly successCriterion: string; readonly riskRule: string; } /** SOFT, directional English — content-hashed, bound into grade provenance (`brief_hash`). * HARD GUARD: the Brief can NEVER enter admission/narrowing. It directs perception and authoring; * it authorizes nothing. */ export interface KernelBrief { readonly text: string; readonly hash: string; readonly version: string | null; } /** * The acting kernel, as the seam carries it. EVERY section is present (absent-not-hidden): an * absent source section projects to an explicit `null`/`[]`, never to a dropped key. */ export interface OversightKernel { readonly mandate: KernelMandate | null; readonly brief: KernelBrief | null; readonly wake: KernelWake | null; readonly dataHealth: readonly KernelVehicleHealth[]; readonly unavailable: readonly string[]; // named unavailable capabilities — listed, never hidden readonly budgetEnvelope: RiskEnvelope | null; readonly ownerActs: readonly KernelOwnerAct[]; readonly engineLog: readonly KernelEngineAction[]; readonly claims: readonly KernelClaim[]; readonly positions: readonly KernelPosition[]; readonly resting: readonly KernelRestingOrder[]; readonly fillsSinceLast: readonly KernelFill[]; readonly budget: KernelBudget | null; readonly plans: readonly KernelPlanState[]; } /* ── 3. The org: Book leaf, Pod node (CONTEXT: Pod / Book / PM / Coverage / Risk) ── */ /** The two model tiers (ADR-0032). TIER (clock/price) is ORTHOGONAL to ORG (allocation). */ export type AgentTier = "strategist" | "watcher"; export const AGENT_TIERS = ["strategist", "watcher"] as const satisfies readonly AgentTier[]; type _AgentTiersExhaustive<_Missing extends never = Exclude<AgentTier, (typeof AGENT_TIERS)[number]>> = true; /** The four escalation triggers (ADR-0032 §4) under the owner-approved HYBRID policy: * - `mandate-edge` AUTOMATIC, fail-closed — the cheap tier never pushes the risk boundary on its * own judgment. It escalates rather than attempt-and-get-refused. * - `brief-flag` ALWAYS-escalate event classes the Brief pre-declares (e.g. a regime break). * - `regime` a structural regime break / SHOCK keyframe — definitionally the strategist's job. * - `uncertainty` the watcher's OWN forced-comment certainty below threshold ("I am out of my * depth; call the PM"). This is the only trigger that is the watcher's judgment. */ export type EscalationReason = "mandate-edge" | "brief-flag" | "regime" | "uncertainty"; export const ESCALATION_REASONS = [ "mandate-edge", "brief-flag", "regime", "uncertainty", ] as const satisfies readonly EscalationReason[]; type _EscalationReasonsExhaustive<_Missing extends never = Exclude<EscalationReason, (typeof ESCALATION_REASONS)[number]>> = true; /** * WHO decided. The four rungs of the attribution ladder (ADR-0032 Resolved 3, extended): * deterministic runtime → watcher → strategist → human. A superset of `AgentTier`, because the * two rungs that are not models still author actions the cockpit must attribute. */ export type AuthorTier = "runtime" | "watcher" | "strategist" | "human"; export const AUTHOR_TIERS = [ "runtime", "watcher", "strategist", "human", ] as const satisfies readonly AuthorTier[]; type _AuthorTiersExhaustive<_Missing extends never = Exclude<AuthorTier, (typeof AUTHOR_TIERS)[number]>> = true; /** * THE TIER STAMP — carried by EVERY action, from day one (owner decision, 2026-07-14). * * TIERS ARE EXPOSED, NOT HIDDEN, and the reason is principled: **Kestrel already refuses * unattributed judgment.** A `MODEL` Field is refused at construction without its receipt * (source + modelVer + confidence — CONTEXT.md *Attribution*: "nothing above CALC goes * unattributed"). An action AUTHORED BY a model deserves the same rule; hiding which tier decided * would be honest about where a NUMBER came from and cagey about where a DECISION came from. * * A Rendering may FOLD the stamp ("just show me the book") — it may never HIDE it. Same discipline * as absent-not-hidden. Because the stamp is in the contract from day one, NO RENDERING NEEDS * REWORK when the second tier lights up. */ export interface Authorship { readonly tier: AuthorTier; /** The deciding model. `null` for `runtime` and `human` — ABSENT, never invented. */ readonly model: string | null; /** The deciding actor's version: the model version for a model tier, the runtime version for * `runtime`, `null` for `human`. Same receipt discipline as a MODEL Field's `modelVer`. */ readonly version: string | null; } /** The cascade, rendered per Book. Positions in time are ORDINALS (`seq`), never clocks. */ export interface AgentTierStatus { /** Rare, frontier: the standing thesis + its last re-frame. The strategist authors the watcher's * View and Brief (owner-approved); a re-brief is a NORMAL supersede. */ readonly strategist: { readonly model: string | null; readonly thesis: string | null; readonly briefHash: string | null; readonly lastReframeSeq: number | null; }; /** Fast, cheap, in-loop: the wake-cadence actor. Its tactical authority is: manage armed Plans, * reload/exit/adjust, size-within-budget, reschedule its own Wake, request a View, stand down — * AND (owner-approved) arm a NEW BOUNDED PLAN within the existing Coverage + Mandate. New * Coverage / thesis / allocation stays STRATEGIST-ONLY. The Gate admits every action regardless. */ readonly watcher: { readonly model: string | null; readonly lastActionSeq: number | null; }; /** A pending escalation: the watcher woke the PM and is waiting for a re-brief. The per-event * record is the first-class `oversight.escalation` event (§3.5.1); this is the standing status. */ readonly escalation: { readonly pending: boolean; readonly reason: EscalationReason | null; readonly atSeq: number | null; }; /** The content-hashed System Profile (ADR-0013) this cascade runs under — the ConfigId whose * cadence axis (strategist calls/day vs watcher calls) is SWEPT on `ev_per_ktoken`, not fixed * (owner-approved). The oversight stream NAMES it; it never computes a grade. */ readonly configId: string | null; } /** A Book = the org LEAF: the only place positions and orders live. */ export interface BookView { readonly bookId: string; /** Coverage = instruments + THE THESIS FOR WHY. Instruments alone are not coverage. */ readonly coverage: { readonly symbols: readonly string[]; readonly thesis: string }; readonly kernel: OversightKernel; // book-state for a Trader readonly tiers: AgentTierStatus; /** The derived status pill — projection only, no new values: severity + relative deadline + * attention. `coalesced` = wakes folded into this one (attention spent, not risk). */ readonly status: { readonly severity: WakeSeverity; readonly deadlineMin: number | null; readonly wakesRemaining: number | null; readonly coalesced: number; }; } /** * A Pod = the recursive node: allocating role + envelope + children. It holds NO POSITIONS and has * NO KERNEL. Its `aggregate` is exactly the child-published org-facts fold the engine already * models (`children(any).<fact>`): a total map of child id → the facts THAT CHILD published. A fact * no child published is ABSENT — and absent is UNKNOWN, which de-arms a PM wake with a logged * reason; it is never a silent `0`. */ export interface PodView { readonly podId: string; readonly envelope: RiskEnvelope; // budgets nest: child ⊆ parent readonly aggregate: Readonly<Record<string, Readonly<Record<string, number>>>>; readonly children: readonly OrgNode[]; } /** Discriminated so a Rendering can walk the tree without guessing. */ export type OrgNode = | { readonly node: "book"; readonly book: BookView } | { readonly node: "pod"; readonly pod: PodView }; /* ── 4. The contract object ───────────────────────────────────────────────── */ /** A rectangular pane of scalar cells — so BOTH Renderings can draw it without inventing a value. * `null` is UNKNOWN and renders as such. NOT pre-rendered ascii. */ export interface SpectatorPane { readonly paneId: string; readonly columns: readonly string[]; readonly rows: readonly (readonly (string | number | null)[])[]; } /** Spectator context: NOTHING AT STAKE (the bare-invocation orientation, ADR-0035 §c). Off the * deterministic path — so live data and a WALL CLOCK are fine HERE, and nowhere else. */ export interface SpectatorFrame { readonly watchlist: readonly string[]; readonly asof: string; // ISO — the ONE permitted wall clock in this contract readonly panes: readonly SpectatorPane[]; } /** * THE contract object. CLI renders it `ascii`/Ink; Web renders it `html`. Same values on both. */ export interface OversightFrame { readonly identity: OversightIdentity; /** Ordinal position on the Bus — NOT a wall clock (determinism). Every `seq` below is ≤ this. */ readonly asofSeq: number; /** The acting view: the PM's Pod. Phase 0 = a degenerate one-Book Pod. `null` on a bare * spectator orientation (nothing at stake, nothing attached). */ readonly pod: PodView | null; /** The spectator view: present on the bare orientation; `null` inside an acting session. */ readonly spectator: SpectatorFrame | null; /** The slot awaiting an answer, if a Book is mid-turn (§3.5). `null` when nothing is pending. */ readonly pending: OversightDelivery | null; /** The recorded conversation up to `asofSeq` — a projection of Bus message events (§3.7), so a * cold Web client re-baselines its chat history from the snapshot, not from a side-channel. */ readonly messages: readonly OversightMessage[]; } /* ── 5. The turn-stream ───────────────────────────────────────────────────── */ /** * A Frame delivery awaiting an answer — the FOUR pentad legs (`sessionId`, `ordinal`, `parentHash`, * `frameRoot`) plus the kernel PROJECTION of what was delivered. The fifth leg — the exact authored * bytes — rides the turn. * * HARD RULE: `frameRoot` addresses the RUNTIME's canonical Frame. The `kernel` here is a VIEW. A * Rendering must never re-hash the projection into `frameRoot`, and the projection must never be * substituted for the delivered Frame. (Mirror of `session/controller-types.ts` `Delivery`, with the * frame body replaced by its Rendering-neutral projection.) */ export interface OversightDelivery { readonly sessionId: SessionId; readonly ordinal: number; // OPEN_ORDINAL (-1) at OPEN; 0,1,… per Wake readonly parentHash: string; readonly frameRoot: string; readonly bookId: string; /** WHICH TIER owns this slot — the cascade is one Session, one Bus, two adapters interleaved at * their wake ordinals by a SINGLE driver (owner-approved). `recordedAgent` replays the whole * cascade byte-identically because both tiers are already above-the-line `Agent`s. */ readonly tier: AgentTier; readonly kernel: OversightKernel; } /** What a committed turn DID to the standing document (mirror of `TurnDisposition`). */ export type TurnDisposition = "armed" | "revised" | "pass" | "stood-down" | "failure"; export const TURN_DISPOSITIONS = [ "armed", "revised", "pass", "stood-down", "failure", ] as const satisfies readonly TurnDisposition[]; type _TurnDispositionsExhaustive<_Missing extends never = Exclude<TurnDisposition, (typeof TURN_DISPOSITIONS)[number]>> = true; /** A committed turn as the cockpit sees it: the content-addressed `TurnEntry` (the full pentad, * from `./session.ts` — imported, NOT re-declared) + its disposition + WHO AUTHORED IT. */ export interface OversightTurn { readonly entry: TurnEntry; readonly disposition: TurnDisposition; /** The tier stamp — `{ tier, model, version }`, present from day one on every action. Replaces a * bare `tier` field: the cockpit must be able to say not just WHICH TIER decided but WHICH MODEL, * at WHICH VERSION (§3.3 `Authorship`). */ readonly authoredBy: Authorship; /** The escalation this turn RAISED, when the watcher woke the PM. `null` otherwise. The CANONICAL * record is the first-class `oversight.escalation` event (§3.5.1) — this is the back-reference. */ readonly escalated: EscalationReason | null; } /** * The watcher woke the PM. Spends ATTENTION, never risk (ADR-0001's Wake invariant), so it is a Bus * fact with a `seq` and never an authored action. */ export interface OversightEscalation { readonly seq: number; readonly bookId: string; /** WHO called — the full stamp, so "which watcher, which version escalated" is answerable. */ readonly from: Authorship; /** WHICH TIER was woken. `"strategist"` today; the ladder allows `"human"`. */ readonly to: AuthorTier; /** WHY (ADR-0032 §4, hybrid policy). `mandate-edge` and `brief-flag` are AUTOMATIC; `uncertainty` * is the watcher's own call, read off its forced-comment certainty. */ readonly reason: EscalationReason; /** The watcher's forced-comment rationale for the call — the legible trace of a fast judgment * (ADR-0032 §8.5). `null` ⇒ none authored; never invented. */ readonly note: string | null; /** The re-brief that ANSWERED it, once it lands: the strategist's superseding turn. `null` while * the escalation is still pending — absent, not hidden. */ readonly answeredBySeq: number | null; } /* ── 6. Oversight is TYPED ACTS, not tickets (ADR-0035 §f, §g) ────────────── */ /** * Every structured human effect is a PM/owner act that lands on the Bus as a seq-ordered, * replayable event — exactly like a wake. There is NO fourth mutation path. * * Approval gates CAPITAL, not trades: `fund` widens the owner envelope (human-signed); inside the * funded envelope agents trade freely at machine speed with NO per-trade approval. Every de-risking * act is ALWAYS FREE — authority only narrows. */ export type OversightAct = | { readonly act: "allocate"; readonly target: string; readonly envelopeR: number } | { readonly act: "arm"; readonly target: string } | { readonly act: "de-arm"; readonly target: string; readonly reason: string } // free | { readonly act: "coverage"; readonly bookId: string; readonly symbols: readonly string[]; readonly thesis: string } // thesis REQUIRED | { readonly act: "fund"; readonly ownerEnvelope: number; /** Human-signed capital authorization. Broker credentials (CLI-direct BYO broker) or a * platform OAuth approval (managed) — the OSS side holds an OPAQUE HANDLE, never a key. */ readonly authorizationRef: string } | { readonly act: "de-fund"; readonly ownerEnvelope: number } // free | { readonly act: "pause"; readonly target: string } // free | { readonly act: "veto"; readonly target: string; readonly reason: string }; // free export type OversightActKind = OversightAct["act"]; export const OVERSIGHT_ACT_KINDS = [ "allocate", "arm", "de-arm", "coverage", "fund", "de-fund", "pause", "veto", ] as const satisfies readonly OversightActKind[]; type _OversightActKindsExhaustive<_Missing extends never = Exclude<OversightActKind, (typeof OVERSIGHT_ACT_KINDS)[number]>> = true; /** The acts that only NARROW authority. These are ALWAYS admitted — de-risking is never gated, * never queued behind an approval, never refused for want of capital. */ export const NARROWING_ACTS = [ "de-arm", "de-fund", "pause", "veto", ] as const satisfies readonly OversightActKind[]; /** * The lifecycle stamp on an ADMITTED act's receipt — is it merely QUEUED (admitted, awaiting a slot to * bind to) or BOUND (in force NOW)? Without it, `ok:true` collapses 'admitted' and 'in force' into one * signal, and for a de-risking veto that gap is a SAFETY LIE: an owner reading `ok:true` reasonably * believes the risk is removed NOW, when the act is in fact pending against a FUTURE slot (kestrel-ysqj). */ export type ActState = "queued" | "bound"; export const ACT_STATES = ["queued", "bound"] as const satisfies readonly ActState[]; type _ActStatesExhaustive<_Missing extends never = Exclude<ActState, (typeof ACT_STATES)[number]>> = true; /** * A submitted act's outcome. On `ok`, `seq` carries the Bus `seq` it landed at (the act IS an event) and * `state` separates 'admitted' and 'in force' so the two are never collapsed into a bare `ok:true`: * - `"queued"` — ADMITTED but NOT YET IN FORCE: a narrowing act ({@link NARROWING_ACTS}) that arrived with * no open slot, waiting to bind the NEXT answered slot. There is no slot yet, so `slot` is absent * (absent-not-hidden — the queued member simply has no `slot` key to invent a value for). * - `"bound"` — IN FORCE NOW: a narrowing act that stood the pending slot down (`slot` = the ordinal of * the delivered slot it bound to), or an envelope act (`fund`/`de-fund`) that took effect immediately * (`slot` is `null` — it binds NO delivered slot, and `null` states that honestly rather than dropping * the key). */ export type ActReceipt = | { readonly ok: true; readonly seq: number; readonly actId: string; readonly state: "queued" } | { readonly ok: true; readonly seq: number; readonly actId: string; readonly state: "bound"; readonly slot: number | null } | { readonly ok: false; readonly reason: string }; /** * An act AS IT LANDS ON THE BUS — the act plus WHO authored it. A human owner act stamps * `authoredBy.tier = "human"` (model and version `null`): the fourth rung of the attribution ladder, * and precisely the event whose ABLATION REPLAY yields `human_alpha` (ADR-0032 Resolved 3, extended). * An act a *strategist* authors (an allocation) stamps its tier and model the same way. Every action * in this contract carries the stamp — no action is unattributed. */ export interface OversightActRecord { readonly seq: number; readonly actId: string; readonly act: OversightAct; readonly authoredBy: Authorship; } /* ── 7. The human-message channel ─────────────────────────────────────────── */ /** Who spoke. `owner` = the human above the root pod (the `human` rung of the ladder — §3.3). * `pm`/`trader` = an agent replying in the conversation (its reasoning already rides the Bus as * JOURNAL); they carry the full `Authorship` stamp on the events they author. */ export type MessageAuthor = "owner" | "pm" | "trader"; export const MESSAGE_AUTHORS = ["owner", "pm", "trader"] as const satisfies readonly MessageAuthor[]; type _MessageAuthorsExhaustive<_Missing extends never = Exclude<MessageAuthor, (typeof MESSAGE_AUTHORS)[number]>> = true; /** * A chat message as a Bus fact. Content-addressed (`messageId = sha256(canonical(author,text,to))`) * so a re-send at the same slot is an idempotent duplicate and never a second event. * * CARRIES NO AUTHORITY. A message can never arm, size, fund, or place. Anything that commits risk is * an `OversightAct` (§3.6) the human explicitly confirms. Chat is sugar over authoring + owner acts. */ export interface OversightMessage { readonly seq: number; // its ordinal on the Bus — the replay key readonly messageId: string; // content address readonly author: MessageAuthor; /** The Book this message is addressed to; `null` = the Pod / the desk. */ readonly to: string | null; readonly text: string; } /* ── 8. The ONE input box (ADR-0035 §e, §f) ───────────────────────────────── */ /** * What the one input box resolved to. PURE and TOTAL — it never throws, never performs I/O, and * never has an authority side effect. Resolution order is fixed: * 1. full Kestrel grammar → `grammar` (executes deterministically) * 2. a bare instrument/expiry → `shortcut` (SHOWS a View — adds NO authority) * 3. a typed act → `act` (the ONLY structured path to authority) * 4. grammar-SHAPED but invalid → `parse-error` (SURFACES; never silently becomes chat) * 5. clear prose → `chat` */ export type InputResolution = | { readonly kind: "grammar"; readonly document: string } | { readonly kind: "shortcut"; readonly view: string; readonly symbols: readonly string[] } | { readonly kind: "act"; readonly act: OversightAct } | { readonly kind: "chat"; readonly text: string; readonly to: string | null } | { readonly kind: "parse-error"; readonly diagnostics: readonly string[] }; export type InputResolutionKind = InputResolution["kind"]; export const INPUT_RESOLUTION_KINDS = [ "grammar", "shortcut", "act", "chat", "parse-error", ] as const satisfies readonly InputResolution["kind"][]; type _InputResolutionKindsExhaustive<_Missing extends never = Exclude<InputResolution["kind"], (typeof INPUT_RESOLUTION_KINDS)[number]>> = true; /* ── 9. The stream event enum (§4.2 — closed, exhaustive-guarded) ─────────── */ export type OversightEventType = | "oversight.snapshot" | "oversight.delivery" | "oversight.turn" | "oversight.act" | "oversight.escalation" | "oversight.message" | "oversight.journal" | "oversight.diagnostic" | "oversight.finalized" | "oversight.failed"; export const OVERSIGHT_EVENT_TYPES = [ "oversight.snapshot", "oversight.delivery", "oversight.turn", "oversight.act", "oversight.escalation", "oversight.message", "oversight.journal", "oversight.diagnostic", "oversight.finalized", "oversight.failed", ] as const satisfies readonly OversightEventType[]; type _OversightEventTypesExhaustive< _M extends never = Exclude<OversightEventType, (typeof OVERSIGHT_EVENT_TYPES)[number]> > = true; /** The two TERMINAL event types that end the stream (mirror of the `operation.completed` / * `operation.failed` pair). `oversight.finalized` ends it cleanly; `oversight.failed` throws. */ export const OVERSIGHT_TERMINAL_TYPES = [ "oversight.finalized", "oversight.failed", ] as const satisfies readonly OversightEventType[]; /** True iff `t` is a contract event type. Anything else is a protocol violation the client MUST * fail closed on (mirror of `isSseEventType`). */ export function isOversightEventType(t: string): t is OversightEventType { return (OVERSIGHT_EVENT_TYPES as readonly string[]).includes(t); } /** The decoded stream event — a discriminated union on `type`. `seq` is present exactly on the * BUS-BACKED members: those events ARE Bus facts and are replayable by seq. */ export type OversightEvent = | { readonly type: "oversight.snapshot"; readonly frame: OversightFrame } | { readonly type: "oversight.delivery"; readonly seq: number; readonly delivery: OversightDelivery } | { readonly type: "oversight.turn"; readonly seq: number; readonly turn: OversightTurn } | { readonly type: "oversight.act"; readonly record: OversightActRecord } // seq rides the record | { readonly type: "oversight.escalation"; readonly escalation: OversightEscalation } // seq rides it | { readonly type: "oversight.message"; readonly message: OversightMessage } // seq rides the message | { readonly type: "oversight.journal"; readonly seq: number; readonly bookId: string; readonly authoredBy: Authorship; readonly text: string } | { readonly type: "oversight.diagnostic"; readonly diagnostic: SessionDiagnostic; readonly ordinal: number } | { readonly type: "oversight.finalized"; readonly seq: number; readonly sessionId: SessionId; readonly tipHash: string; readonly artifacts: readonly string[] } | { readonly type: "oversight.failed"; readonly reason: string }; /** * The DECODED-union ↔ enum guard. `_OversightEventTypesExhaustive` (above) pins the string union * `OversightEventType` ↔ the tuple `OVERSIGHT_EVENT_TYPES` in both directions. This one closes the * remaining link: the discriminants of the DECODED object union `OversightEvent` ↔ `OversightEventType`, * in BOTH directions. Together they make the whole chain — tuple ↔ string union ↔ decoded union — a * closed loop: a member added to (or removed from) `OversightEvent` without touching the enum * (`_ExtraInDecoded`), or an enum member with no decoded case (`_MissingFromDecoded`), makes a default * non-`never` and FAILS THE BUILD here. This is the one-way drift a self-referential runtime check * (a tuple compared to a variable typed FROM that tuple) structurally cannot catch. */ type _OversightEventDiscriminantsExhaustive< _MissingFromDecoded extends never = Exclude<OversightEventType, OversightEvent["type"]>, _ExtraInDecoded extends never = Exclude<OversightEvent["type"], OversightEventType>, > = true; /* ── 10. The backend seam (§5.1) ──────────────────────────────────────────── */ /** Implemented LOCALLY (OSS: in-process controller + local BYOK agent + BYO broker) and REMOTELY * (platform: hosted OAuth, managed keys, funded accounts). The CLI (Ink) and the Web dashboard are * two CLIENTS OF THIS — that is the whole cross-repo seam. */ export interface OversightBackend { readonly kind: "local" | "remote"; identity(): Promise<OversightIdentity>; /** The current view — a PURE PROJECTION of harness state. */ frame(): Promise<OversightFrame>; /** The live stream. `after` is the OPAQUE cursor; absent ⇒ from the current snapshot. */ stream(after?: string): AsyncIterable<OversightEvent>; /** Parse-first resolution of the one input box. PURE, TOTAL, fail-closed (§3.8). */ resolveInput(text: string): InputResolution; /** A typed act → a Bus event. Chat can NEVER reach this; only an `act` resolution can. */ submitAct(act: OversightAct): Promise<ActReceipt>; /** Post an owner message → a Bus event (§3.7). Carries NO authority. */ say(text: string, to: string | null): Promise<{ readonly seq: number; readonly messageId: string }>; /** Answer a delivered slot — the ONE thing that crosses into the graded Bus as an authored action. * The response vocabulary is the EXISTING `AuthoredResponse`; no fourth kind is added. */ respond( r: { readonly kind: "authored"; readonly document: string } | { readonly kind: "pass" } | { readonly kind: "stand-down"; readonly reason: string }, ): Promise<{ readonly ok: true; readonly turn: OversightTurn } | { readonly ok: false; readonly diagnostic: SessionDiagnostic }>; }