Review round 1. flysim reads FLY_FEED_VIA case-insensitively, so check 2 must
too: with Bus in fly.env it read flysim's zeroed counters and would have
escalated to restarting flystage and flycast every pass. It now lowercases.
05-deploy.sh runs the value through feed_via_normalize (lib/common.sh) and
dies on anything but direct|bus, writing the lowercased word, so a typo is a
deploy refusal instead of a flysim boot loop.
lint's fly.target check read only the first physical Wants=/Requires= line;
target_pulls joins backslash continuations and drops comments, with a
fixture that names a unit only on a continuation line. The cpuset loop writes
a flyedge drop-in on the page CPUs, and lint holds it.
Row 58's pad was GO OBJECTIVE into the Pewter Gym and GO OUT straight
back out for 25 minutes, diluted by eight other names, every macro done:
ten distinct names, so the four-name sequence rule could not fire, no
macro near 95%, nothing refused or blocked, and the exploration count
flat. What the window did not have was a reward event.
The stream carries the reward events beside the macros, and one more
rule reads them behind the same no-new-ground gate: WD_LOOP_BUSY_MIN
(100) decisions and no reward in the window, on two probes running, is
'unrewarded'. fly_loop_rewards is exported and loop.json carries
window.rewards. Run against the live row-58 log it flags (211 decisions,
0 rewards, 10 names) where the rules before it did not. Still never
acts: the fixture's two new cases restart nothing.
flyedge.service runs /opt/fly/current/fly-edge After= and Requires=
flysim.service, with its metrics on loopback :9102 and a
ConditionPathExists so a release without the binary leaves it inactive. It is
in no target and 07-enable.sh does not enable it; the header has the switch
and the way back.
build-flysim.sh also builds fly-edge beside the flysim binary and
package-release.sh ships it when present. 05-deploy.sh writes
FLY_FEED_VIA (default direct) into fly.env, flysim.service names
FLY_BUS_DIR=/run/fly/bus and tmpfiles creates it. Watchdog check 2 reads the
feed counters from whoever serves the feed: flyedge when fly.env says bus.
lint.sh holds all of that, and drives check 2's choice against a fixture.
Row 57's pad was one button refused every hold for two hours: one start
and one name in ten brain minutes, so neither the sequence rule nor the
dominance rule could fire, and the coordinator's own watcher saw it
first. Check 10 now reads every macro outcome in the window. The fly's
decisions are its starts and its refusals; `stalled` flags 90% of 20+
decisions ending refused, blocked or timed out, and `zero-progress`
flags decisions with no `done` among them on two probes in a row. Both
sit behind the same no-new-ground gate as the old rules.
fly_loop_refused, fly_loop_blocked and fly_loop_done are exported, and
loop.json carries window.decisions and window.outcomes. It still never
acts: the lint fixture asserts no unit is restarted across six cases,
two of them new (the row-57 log, and a zero-progress log).
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.