flybrain/infra/env
alex 23a4d7379b rewards: a catch reward, adapter v6 with a v5 migration, and a rung restart
The operator's decision of 2026-09-22: pay the fly for keeping a wild Pokemon,
bump the adapter properly, and restart the live run from an early checkpoint
rather than from scratch.

The rule. `catch` is the catalog's ninth kind, appended so the key order
`counts` serializes in does not move. 0.30 for a species this run had never
owned, 0.10 for a repeat, three payouts per species for the lifetime of the
ledger; the `species` rule is untouched, so a first catch of a new species pays
0.80 across two kinds. The catch is read from `wCapturedMonSpecies` ($d11c),
whose comment in ram/wram.asm is "0 if no mon was captured": ItemUseBall zeroes
it before every throw and writes wEnemyMonSpecies into it only on the branch
that keeps the Pokemon, and UseBagItem's `.returnAfterCapturingMon` zeroes it
again and sets wBattleResult to 2 -- a value written on exactly two paths in
the game, that one and a link battle whose opponent ran. Both are required, so
a byte read out of a half-initialised battle cannot pay. Not wPartyCount: a
catch with a full party raises wBoxCount instead, and wPartyCount also rises
for a gift, a trade and a PC withdrawal.

"Never owned this run" is the `species` payout inside the same battle, because
nothing else can set a Pokedex bit during one. It is not read off the captured
species byte: that is the cartridge's internal index while the owned bitset is
by Pokedex number, and nothing in WRAM converts between them.

The address was resolved by tools/resolve_wram.py, not written by hand. The
tool needed NUM_TMS and NUM_HMS, which the decomp defines through its `const`
enumeration, so it now counts them from the file's own add_tm/add_hm
definitions and cross-checks NUM_TMS against the literal the same file
declares.

The feed's kinds are closed, so `catch` publishes on `wildwin` and nothing in
packages/feed or apps/stage changed. Deliberately not `pokedex`: the `species`
rule already pays for the bit the same catch sets. The stage's ticker copy is
keyed on the feed kind, so a catch row reads "wild win" -- stated in
docs/rewards-learning.md rather than left to be discovered.

v5 -> v6. STATE_VERSION stays 4: the rule adds one counter, `catchCounts`, and
changes nothing else, so a v5 state restores with it empty. That migration is
opt-in and needs all three of: the adapter segment being the only difference
between the two compatibility strings, the running adapter listing the
checkpoint's adapter in `migrates_from()`, and the deploy naming it in
FLY_ACCEPT_ADAPTERS. flysim applies the rule at restore and 05-deploy's gate
applies the same rule before it flips the symlink, writing the variable into
fly.env so the two cannot disagree.

The restart. infra/bin/fly-reset-to-milestone <N> archives both stores to a
dated directory, rewrites milestone-<N>.checkpoint with the ratchet's attempts
and recoveries at zero, installs it as the newest generation of both stores,
clears the milestone archives above N and the event log, and prints what it
did. It refuses while flysim is running and refuses a rung the run never
reached. The envelope work is in flysim::reset (`flysim
--reset-to-milestone N`); the shell script is the operator's wrapper.

Tests: catalog values and order; a synthetic WRAM trace of a catch (new,
repeat, cap, already-owned species, trainer/Safari/old-man/missed-ball
negatives, rollback replay); a v5 state restoring with the counter at zero; a
v5 checkpoint fixture accepted with the opt-in and refused without it; the
reset tool against copies of a state dir in temp directories; and a ROM-gated
catch from a rung-9 forest checkpoint, driven by the shipping THROW BALL macro.

The compatibility string differs from main's in exactly one segment, checked by
splitting both on `/`: pokered-unique8-v5 -> pokered-unique8-v6.
2026-09-22 17:30:00 +00:00
..
example.env rewards: a catch reward, adapter v6 with a v5 migration, and a rung restart 2026-09-22 17:30:00 +00:00
README.md flybrain v0.4.0: public tree (history retained privately) 2026-09-21 15:09:46 +00:00

infra/env — the env files, and why the real ones are not here

infra/env/example.env is the only env file in this repo. It lists every knob the infra/ scripts read, with placeholder values and the comments that say what each one costs. It is a template: it will not deploy anything.

The real env files live in the operator's infra repo, not here. This repo is public, and a real env file names the host, the container id, the LAN address, the Twitch channel and the pass entry that holds the stream key. Keeping them out is the point of the de-PII sprint (inventory in the operator's infra repo); infra/tests/lint.sh refuses those patterns on every run.

Handing a script an env file from outside the repo

Every script takes the env file as its first argument, and that argument may be any absolute or relative path — nothing resolves it against this directory:

# on the host, as root
install -d -m 0750 /etc/fly/env          # or wherever you keep them
# ... write the real file to /etc/fly/env/fly-pokemon.env ...

infra/provision.sh /etc/fly/env/fly-pokemon.env
infra/05-deploy.sh /etc/fly/env/fly-pokemon.env <release-tarball>
infra/verify.sh    /etc/fly/env/fly-pokemon.env

Or set FLY_ENV_DIR once and pass a bare name; load_env resolves an argument that is not itself a readable file against that directory:

export FLY_ENV_DIR=/etc/fly/env
infra/provision.sh fly-pokemon.env
infra/verify.sh    fly-pokemon.env

FLY_ENV_DIR is only a convenience for typing. If the file is not found either way the script dies naming both paths it tried.

Keeping a real env in step with this template

example.env changes whenever a knob is added. After pulling a flybrain bump:

diff <(grep -oE '^[A-Za-z_][A-Za-z0-9_]*=' infra/env/example.env | sort -u) \
     <(grep -oE '^[A-Za-z_][A-Za-z0-9_]*=' /etc/fly/env/fly-pokemon.env | sort -u)

Lines only on the left are knobs the real file has not been told about yet. load_env requires CTID, HOSTNAME, IP, GAME and PUSH_TARGET; ROLE defaults to dev.

What must never be in an env file

Secrets. PASS_KEY holds the name of a pass entry, never a key. The Twitch app id/secret arrive as the systemd credential twitch-app, installed by infra/06-secrets.sh from pass on the operator box; the OAuth tokens live in the container as /var/lib/flybridge/tokens.json, mode 0600. ROM_SHA256 is a digest, and the ROM it describes is never committed, copied into the repo, or shown on stream.