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 newsrc/<kind>.rs(Facts/Answer/Memory/Key/Decision impl) plus oneEngine<YourKind>, not a request built inline againstjev-httpdirectly. - 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::askrefuses 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::askalways falls back toDecision::fallbackand the bot plays on. Do not retry around either, and do not sleep for a throttle:Enginerecordsretry_inand 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, andIDSis fixed at six because the shim never offers more. The panel, the log, andChooser::overrides(goal!= 0means Jev's pick differed) rely on this.- Native only (this crate links the C bot and a socket, through
zbanksandjev-http); never add it to a wasm build. Apackages/crate that needs the network or a window is the exception here, same reasoning aszbanks/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'sAnsweror onHowchanges what a line looks like on disk;Engine::load_historyalready treats a line it cannot parse, or whosekinddoes 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_kindinsrc/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 nokindfield. 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 byHISTORY_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::overridesis not part of the sharedTotals. Whether anAnswerdiffers from "upstream's own pick" depends on that decision kind's own convention (goal index 0 here);Engine/Totalsstay 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 ownChooser-equivalent, not bolted ontoTotals.Decision::Memoryis deliberately not alwaysDecision::Answer.goal_choicekeeps 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 setMemory = Answerand havereuseignore the sampling closure.goal_choice::Factsreaches SRAM/WRAM without touchingpackages/zbanks(goal_choice::Snapshot, read fromconsole.wram()): the CALLER (apps/native's tick,apps/zbanks's loop) callsChooser::set_snapshotonce a frame, BEFOREzbanks::Bot::tick, since the C shim's goal-choice hook can fire synchronously inside it. This is deliberate - thezbanks::GoalChoosertrait's signature is not this module's to change, and a second, purely-additive channel avoids ever needing to. A caller that forgets to callset_snapshotis not a compile error (the field defaults to all-zero), so a NEW caller ofgoal_choicemust be checked against this, not assumed to inherit it.Facts::sentences(), notwords::offers()'s own sentences, is what goes to Jev and intoAnswer. 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. ExtendDUNGEON_LORE, notwords.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 cratepackages/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. MODELis 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.