For agents, on top of README.md, which they read first.

  • A new Jev call is a new module implementing Decision, never an ad-hoc request built somewhere else. If a call site wants to ask Jev something and there is no decision kind for it yet, the fix is a new src/<kind>.rs (Facts/Answer/Memory/Key/Decision impl) plus one Engine<YourKind>, not a request built inline against jev-http directly.
  • Off must stay upstream. With no chooser installed the goal-choice hook is NULL and the bot is byte-for-byte upstream's; a change here must never install one implicitly. The apps default to off. A future decision kind that drives some other part of the bot owes the same guarantee.
  • Every failure is upstream's pick, never a stop. jev_http::Jev::ask refuses on the self-clearing guards (Failure::Throttled) and on what only a human can clear (Failure::Budget: the vendor said out of credit, or the ledger cannot be read; there is no self-imposed lifetime cap since 2026-09-22); Engine::ask always falls back to Decision::fallback and the bot plays on. Do not retry around either, and do not sleep for a throttle: Engine records retry_in and simply does not ask again before it (throttled_until).
  • goal_choice's option order is the C shim's: index 0 is upstream's own pick, the rest cheapest first, and IDS is fixed at six because the shim never offers more. The panel, the log, and Chooser::overrides (goal != 0 means Jev's pick differed) rely on this.
  • Native only (this crate links the C bot and a socket, through zbanks and jev-http); never add it to a wasm build. A packages/ crate that needs the network or a window is the exception here, same reasoning as zbanks/mcp/replay - see ../CLAUDE.md.
  • The key only ever arrives through op-env-run; see the repo CLAUDE.md.
  • Record<A>'s JSON shape IS the log's schema - there is no second, hand-written mapping to keep in sync. Adding or renaming a field on a decision's Answer or on How changes what a line looks like on disk; Engine::load_history already treats a line it cannot parse, or whose kind does not match, as belonging to some other kind or an incompatible shape (skipped, not an error), so this is safe to evolve - but a field rename silently stops matching every line written before it, which is a real (if harmless) loss of that window's seeded history, not a compile error to catch it.
  • kind's default (default_kind in src/lib.rs) is hardcoded to "goal_choice", not left generic. Every line ever written before this crate had more than one decision kind WAS a goal choice, so that is the one sound default for an old line with no kind field. Do not change it to something "more neutral" - there is no neutral answer for what an old line meant, only the correct one.
  • Engine::history()/load_history() are bounded by HISTORY_CAP; the log they read from and write to is not. Raising the cap only changes how much a restart re-seeds into memory; it does not recover anything, the full past is already on disk in the log either way.
  • goal_choice::Chooser::overrides is not part of the shared Totals. Whether an Answer differs from "upstream's own pick" depends on that decision kind's own convention (goal index 0 here); Engine/Totals stay honestly generic and do not guess at it. A future decision kind with its own notion of "differed from the default" keeps that count the same way - beside its own Chooser-equivalent, not bolted onto Totals.
  • Decision::Memory is deliberately not always Decision::Answer. goal_choice keeps a probability per underlying goal IDENTITY (kind|node|screen), not per the sentence built for one particular call, because the next call's offer grouping can name the same goals with different wording once Link has moved. A decision kind whose reuse really is "the exact same answer, unchanged" can set Memory = Answer and have reuse ignore the sampling closure.
  • goal_choice::Facts reaches SRAM/WRAM without touching packages/zbanks (goal_choice::Snapshot, read from console.wram()): the CALLER (apps/native's tick, apps/zbanks's loop) calls Chooser::set_snapshot once a frame, BEFORE zbanks::Bot::tick, since the C shim's goal-choice hook can fire synchronously inside it. This is deliberate - the zbanks::GoalChooser trait's signature is not this module's to change, and a second, purely-additive channel avoids ever needing to. A caller that forgets to call set_snapshot is not a compile error (the field defaults to all-zero), so a NEW caller of goal_choice must be checked against this, not assumed to inherit it.
  • Facts::sentences(), not words::offers()'s own sentences, is what goes to Jev and into Answer. It appends vanilla dungeon knowledge (DUNGEON_LORE) when an offer's sentence already names a place the bot resolved - never for "somewhere never visited", which is honestly all that is known about an unexplored door. Extend DUNGEON_LORE, not words.rs, for a new named place: the wording change stays isolated here (user, 2026-09-22).
  • The behaviour-preservation test in goal_choice.rs (the_request_sent_to_jev_is_unchanged_by_the_rewrite) compares against a request captured from the pre-rewrite goal-choice code (the crate packages/zbanks-jev, which this crate replaced on 2026-09-22), byte for byte. Any future edit to the wording or shape of the goal-choice request should update that golden deliberately, in the same commit, with a note saying why the wording changed - never adjust the test to match new output without reading the diff.
  • MODEL is pinned (jev-1.13.0), not an alias. FAVOURITE_THRESHOLD (0.70) was tuned against it; moving the pin is a change to re-measure, not a version bump.