# Five probes on the running game — and a harness defect the probes found in themselves Lane H, 2026-09-08. Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs. Engine worktree `wip/probes`, predictions in `sots-engine/docs/H-probes.md`, **committed before the build** (method rule 2). VM140 held for the duration; released at the end. **The headline, in order of what depends on what.** 1. **The determinism oracle reproduces exactly.** `bb4fd9ac…` and `978041ac…`, byte for byte, after everything this session changed. §1. 2. **A hook in this lane's own instrument changed the game's output**, and it was caught by comparing against a save lane Z had already produced. Bisected to **one address**. Every measurement below was then re-taken on a configuration proved byte-neutral against that save. §2. 3. The four tail callees that can draw are **entered on every single turn** and gated *inside*; the three inner functions that hold the draws are entered **zero** times. "Never observed firing" was about a gate, not about reachability. §3. 4. `CreateRaidEncounter` **is called** — twice on one measured turn — and **draws nothing**. The two hypotheses a word count could not separate are separated: the candidate list is empty. §4. 5. The Zuul prediction holds exactly: **7 calls and 7 words per site, 14 not 16.** The raid loop iterates the player vector. §5. 6. `EncounterDetect::AssignContacts` cannot fire on our corpus for a reason nobody had measured: its caller is never called, because `EncounterDetect_Run` receives an **empty** record vector. The 2-word detection residual is somewhere else entirely, and §6 names the function it is in. --- ## 1. The oracle, regenerated — and it reproduces Lane N's End Turns overwrote the three autosave *contents* on VM140 and said so. The file set was never disturbed, `ref-turn2.sav` still hashed `ab4ac2d7e2977260…` (checked before the run), and all 11 curated saves were in the repo, so nothing was lost — but the reference pair had to be remade. Load `ref-turn2.sav`, one End Turn, build `recap-7584bad-20260908T0615Z`, `hooks=trace`: | file | size | sha256 | historical value | |---|---|---|---| | `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948` | **identical** | | `(Autosave).sav` | 67,219 | `978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921` | **identical** | **Both reproduce.** The turn-2 state on screen (Imperial Savings 289,688) and the turn-3 state (532,369) match `determinism-oracle.md` as well. This is worth more than a repeat. The process the oracle now runs in is not the one it was measured in: nineteen template hooks and seven entry-point detours installed instead of one asm stub, an x87 control-word module, eight save files where there were four, and a Zuul game played to turn 64 in the same directory in between. **The oracle is revalidated across all of that**, which is what every Phase-2 comparison downstream of it needed. Files: `verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav`. **And the same two files were put back on the VM at the end**, so the three autosaves once again carry the documented oracle bytes. That is a restore of bytes this lane produced by an actual run today, not a substitution of an older snapshot — the distinction lane N was right to insist on. --- ## 2. The instrument perturbed the game. Bisected to one address. This section is first among the probes because it invalidates a run and because nothing in this campaign has ever tested it. ### 2.1 How it was caught Three End Turns from `ref-turn2` under the full lane-H build gave per-turn word counts of **19, 18, 19**. Lane Z's per-site table for the same three turns of the same save reads **19, 18, 20**. One word apart on the third turn — the kind of difference that is easy to explain away as workload. It was not explainable, because the **input was byte-identical**. The run's `(Autosave EndTurn).sav` (the turn-4 pre-turn state) hashed `8648957fecf5041233f9add4c8173f78981f026f133ecdbecdb89ddd10917048`, which is exactly lane Z's `z2-endturn.sav`. Same bytes in; different bytes out: | | `(Autosave).sav` after the turn-4 → turn-5 End Turn | size | |---|---|---| | lane Z (`z2-autosave.sav`, and `z-t5-autosave.sav` from a second lane-Z run) | `b2124798470d85ea…` | 68,267 | | lane H, full build | `058656787877cbee…` | 68,263 | Both sides are reproducible: lane Z's value appears in two of its runs, and lane H's appeared in two of its own (three consecutive End Turns, and a fresh load of `z2-endturn.sav` followed by one End Turn). So this is not noise in either direction. ### 2.2 The bisect `z2-endturn.sav` was pushed to the VM and the same single End Turn run five more times, changing one thing each time. A `probes=` key was added to `shim.cfg` for exactly this purpose — an instrument that can change what it measures has to be switchable, or the question cannot be asked at all. | configuration | `(Autosave).sav` | verdict | |---|---|---| | `hooks=off` — the shim installs **nothing** | `b2124798…` | the un-instrumented game | | all template hooks + 7 draw-site detours, `probes=0` | `b2124798…` | **neutral** | | + probes 0–3 (the four phase-23/33 vtable callees) | `b2124798…` | neutral | | + probes 4–7 (three inner callees, `CreateRaidEncounter`) | `b2124798…` | neutral | | + probe 8 (`ServerTradeManager_GenerateTradeRaidEncounters` 0x00893290) | **`058656…`** | **perturbs** | Two things fall out, and the first is the more important: * **The un-instrumented game agrees with lane Z, not with lane H.** So lane Z's ledger — 7 draw-site detours and 6 nested template hooks — **is behaviour-neutral on this pair**, and its published numbers stand. This is the first time any lane has checked that, and it came out the right way. * **Hooking `GenerateTradeRaidEncounters` at 0x00893290 with a MinHook detour changes the turn.** Specifically it *suppresses* the `ServerPlayer::RollResearchEvent` draw at 0x0088df4f: the clean run spends 20 words with that site firing once, the perturbed run spends 19 with it not firing at all. ### 2.3 What the mechanism is not The obvious relocation hazard is **absent**. The bytes at 0x00893290 are ``` 55 push ebp 8b ec mov ebp,esp 6a ff push -1 <- the 5-byte patch boundary falls exactly here 68 37 c3 99 00 push 0x99c337 ; SEH scope table 64 a1 00 00 00 00 mov eax,fs:[0] ; ... standard MSVC __try + /GS prologue ``` so MinHook's five bytes land on a clean instruction boundary, the relocated prologue is three position-independent instructions, and there is no branch target inside the patched range. A mangled prologue is therefore **not** the explanation, which makes this more interesting rather than less. **The mechanism is undetermined and is recorded as an open item, not guessed at.** What is odd, and is the thing to chase: `RollResearchEvent` runs *earlier in the turn* than trade-raid generation, so hooking 0x00893290 changed a decision taken before the hooked function was reached. The cheapest next probe is a compare-mode hook on `RollResearchEvent` in both configurations, reading the threshold and the progress value it tests, on the same input save. ### 2.4 What this costs, and what it does not * Every number in §§3–6 was re-taken at `probes=8`, the largest configuration proved byte-identical to the un-instrumented game on this save. Nothing below rests on the perturbed run. * The perturbed run's trace is kept — `verify/traces/h-probes-human-perturbed-t3-5.jsonl.gz`, report `verify/results/shim/probes/human-t3-5-perturbed.txt` — labelled as such, because it is the evidence for §2 and because its probe *entry counts* agree with the clean runs on every row. * **The general lesson is a method rule waiting to be written.** A behavioural harness has to be checked against the oracle, not just against itself. It cost one save comparison to find this; it would have cost a published wrong number to find it later. The `probes=` key exists so the check is cheap to repeat: any lane adding detours should run one End Turn from `z2-endturn.sav` and confirm `b2124798470d85ea…` before trusting its own measurements. --- ## 3. The four tail callees — reached on every turn, gated inside Lane V2 closed lane K's tier-4 blind spot: four of the eleven phase-23/33 vtable targets on `StrategyServer+0x158`/`+0x15c` reach a draw on the strategic generator, at eight instructions. Lane Z measured 0 tail words on eight turns. Between those two facts sat a question a word count cannot answer — *not reached*, or *reached and gated*? Register-transparent asm stubs (`pushfl; pushal; push idx; call; popal; popfl; jmp trampoline`) count entries without assuming a calling convention. Two saves, three measured turns: | address | what | Human t5 | Zuul t24 | Zuul t25 | |---|---|---|---|---| | 0x008877b0 | `ServerSpyManager` slot 13 | **1** | **1** | **1** | | 0x0088db80 | `ServerSpyManager` slot 14 | **1** | **1** | **1** | | 0x0088ef80 | `ServerTradeManagerImpl` slot 13 | **1** | **1** | **1** | | 0x0082cca0 | `ServerTradeManagerImpl` slot 15 | **1** | **1** | **1** | | 0x008408e0 | `SpyManager_Slot13RngCallee` | 0 | 0 | 0 | | 0x00820ca0 | `TradeManager_Slot13RngCalleeA` | 0 | 0 | 0 | | 0x0088b440 | `TradeManager_Slot13RngCalleeB` | 0 | 0 | 0 | **P2a and P2c both hold.** The phase-23/33 dispatch block is unconditional, exactly as lane V2 read it; and the gate that keeps the draws from firing is **inside each outer body, above the inner call**. No draw-site row appeared at any of the eight return addresses on any turn, and the tail's bracket cost stayed 0 — reproducing lane Z's number under an instrument that can now say *why*. So the correct statement is no longer "the tail draws nothing" and not even "the tail draws nothing on these turns". It is: **the tail's four draw-bearing callees run on every turn and return before reaching their draws.** That is a much stronger negative, and it converts the open question from "which turns reach these functions" to "what does the gate in each body test". ### 3.1 Three of the four gates are an empty container; the fourth is not, and it was wrong Read from the bodies (each confirmed a real function start with exactly one caller): | callee | the loop it drives | empty when | |---|---|---| | `ServerTradeManagerImpl` slot 15 | vector at `tradeManager+0x3c .. +0x40` | no trade routes | | `ServerSpyManager` slot 14 | vector at `spyManager+0x10 .. +0x14` | no spy programs | | `ServerSpyManager` slot 13 | 1,869 bytes, same manager, not fully read | — | | **`ServerTradeManagerImpl` slot 13** | **`GetServer()+0x64 .. +0x68` — the FLEETS vector** | **no fleets** | That last row produced a competing prediction, **P2b-alt**, committed after the bodies were read and before any run: on a save with fleets, slot 13's loop body runs, `TradeManager_Slot13RngCalleeA` fires, and the tail cost is non-zero. **P2b-alt is falsified, and cleanly.** `zuul-turn23-fleet23.sav` is named for its 23 fleets; slot 13 was entered on both its turns and 0x00820ca0 was entered **zero** times. So the fleet loop is not the gate — the per-fleet test chain that precedes the call at 0x0088f036 is, and it rejects every fleet on a 23-fleet save. That is a narrower and more useful place to look than "the loop never runs", and it is the kind of wrong prediction that pays for itself: had P2b been the only one written down, its confirmation would have been read as "no trade routes, no spies" and the fleet loop would have gone unexamined. ### 3.2 What is still a hypothesis (rule 6) **Whether any of the eight draw sites ever fires** is still unexercised. The workload that would settle it — a game with live trade routes and a live spy program — was **not built**, and it is worth saying exactly why rather than vaguely: * SotS 1 trade income requires trade-station construction and the tech behind it; from `ref-turn2` (turn 2, 1 colony, 0 techs) that is tens of End Turns at 30–60 s each plus a Build/Research click path per turn. The newest Zuul save in the corpus (turn 23) still has **no** trade routes: slot 15's loop body did not execute on it. * The corresponding spy state is worse — no lane has yet identified which UI produces an entry in the `spyManager+0x10` vector, so there is no click path to write down, only a container to watch. * What this lane did instead was cheaper and, for the map, better: it established that the containers are the gate for two of the four and that the fourth is gated per-fleet. A future lane that builds the workload now knows which two vectors to watch fill, and can confirm the workload took *before* spending a turn measuring. --- ## 4. `CreateRaidEncounter` — the two hypotheses, separated A successful trade-raid roll calls `ServerTradeManagerImpl` slot 17, `CreateRaidEncounter` 0x008938a0, which draws a `NextInt` at 0x008939ee to pick a target but returns at 0x0089391c **without drawing** when the candidate vector is empty. It cost 0 words on all three of lane Z's turns, which lane Z read as consistent with either "no roll succeeded" or "the candidate list is empty". **A correction to the prior first (rule 11).** Lane Z wrote that no roll succeeding is "≈11% on the defaults, so three quiet turns in a row is unremarkable". 11% is the probability of a quiet *turn*: `0.8^8 × 0.95^8 = 0.1113`. Three consecutive is `0.00138`, about **1 in 720**. The two hypotheses were never close to equally likely, and the expected number of successes per turn is `8 × 0.2 + 8 × 0.05 = 2.0`. **Measured:** | turn | `CreateRaidEncounter` entries | words drawn | |---|---|---| | Human t4 → t5 | 0 | 0 | | Zuul t23 → t24 | **2** | **0** | | Zuul t24 → t25 | **1** | **0** | | Human t3/t4/t5, perturbed run | 0 / 1 / 0 | 0 | **Settled: rolls succeed at about the predicted rate, and the candidate list is empty every time.** Four successful rolls were observed across five measured turns and not one of them reached the `NextInt` at 0x008939ee — no draw-site row at return address 0x008939f3 on any turn. For the standalone this is the useful form: on a corpus like ours the cost of a successful raid roll is **0 further words**, and the row lane Y flagged as "the one that will bite" does not bite yet. It will bite the first turn a raid candidate exists, and the candidate list is built by 0x0083b110 — a function no lane has read. **One caveat this lane found and must state.** `CreateRaidEncounter` is slot 17 and is reached by virtual dispatch, and `GenerateTradeRaidEncounters` is **not its only caller**: `0x007d5150`, the other subtree of `DetectEncounters` (see §6), also dispatches slot 17 — `(**(code **)(**(int **)(S + 0x158) + 0x44))(out, player, 1, records)` — on the branch where its own per-player roll returns 0x12. So an entry count of 2 does not prove two *trade-raid* successes; it proves two slot-17 calls from one of two known sites. Separating them needs the return address, which an entry counter deliberately does not capture. `tail-rng-ledger.md` §11.1's "called from `GenerateTradeRaidEncounters` at 0x0089345e / 0x00893548 / 0x00893603" is incomplete as a list of callers. --- ## 5. The Zuul word count — 14, exactly as predicted Lane Z's free prediction, restated by lane Y as a consequence of its `trade_raid.cpp` model: the two `Chance` sites in `GenerateTradeRaidEncounters` cost one word per entry of `StrategyServer::Players`, which holds 8 on the Human saves and 7 on the Zuul ones. Lane N ran a Zuul End Turn with the RNG hooks off, so it could not answer. `zuul-turn23-fleet23.sav`, two End Turns, `probes=8`: | site | Human t5 | **Zuul t24** | **Zuul t25** | |---|---|---|---| | 0x00893426 `Chance` (`TRADE_RAID_ODDS_PLAYER`) | 8 calls / 8 words | **7 / 7** | **7 / 7** | | 0x00893513 `Chance` (`TRADE_RAID_ODDS_NPC`) | 8 calls / 8 words | **7 / 7** | **7 / 7** | | trade-raid block total | 16 | **14** | **14** | | turn total (attributed = measured) | 20 | **18** | **18** | **P5 holds in full**, and it is a prediction that survived a changed workload rather than a repeat of a recording: 7 is a number no earlier run produced. The loop iterates the server's player vector and nothing else — not the `TeamRecord` vector it is handed, not a species list, not a fixed NPC count. Lane Y's `trade_raid.cpp`, which counts `NumPlrs`, is right. The Zuul turn's remaining 4 words are 2 detection (§6) and **2 research** — both `ProcessResearch` sites (0x00587888 and 0x005878bb) fired on both turns, which is the second research site's first appearance in a site table. --- ## 6. `DetectEncounters` — the residual is not the `AssignContacts` family, and here is where it is Lane Y's standalone leaves a 2-word residual per turn, attributed by lane Z to 0x0050329d (`NextFloat`, depth 4) and 0x007929a4 (`NextInt`, depth 3), and refused to model it: *"fitting 2/turn to three observations is not a model."* Lane I bounds the containing family at `|contacts| × |detectors|` and showed `EncounterDetect_AssignContacts` draws one inlined `NextFloat` per (contact, detector) trial. Lane Z found `AssignContacts` never ran. `EncounterDetect_ProcessTeamRecord` 0x007ca640 — its only caller — was hooked, with lane I's gate predicate, contact count and detector count recomputed from the record at hook entry. **It never ran either. On any turn, on either save.** The hook installed (`MH_OK` for create and enable, logged), was registered in the trace's meta line, and emitted **zero records** across five End Turns, while the entry probe on `EncounterDetect_Run` recorded **one call per turn, every turn**. `EncounterDetect_Run` calls `ProcessTeamRecord` once per 0x74-byte record. One call, zero records means the vector it receives is **empty**. So: * **`AssignContacts` cannot fire on our corpus, and the reason is one level higher than anyone thought.** It is not that the `+0xfc` gate fails on the records — there are no records. Lane I's `|contacts| × |detectors|` bound is correct and simply has nothing to bound yet. * **P3b is falsified**, exactly as its falsification clause anticipated, and the depth reasoning that produced it was wrong about the *path*, not the depth. ### 6.1 Where the two words actually are A call-graph walk from both residual sites upward lands in the same place, and it is not `EncounterDetect_Run`: ``` StrategyServer::DetectEncounters 0x007d7f70 (the last phase of ProcessTurn) ├─ 0x007d8469 GenerateTradeRaidEncounters [virtual] 16 words (14 on Zuul) §5 ├─ 0x007d8470 EncounterDetect_Run 0x007cb080 0 words -- empty record vector └─ 0x007d860b 0x007d5150 ** the 2 words ** ├─ 0x007d52b3 0x0078cc70 -> 0x0078ccb1 0x00503200 -> NextFloat 0x0050329d 1 word └─ 0x007d5311 / 0x007d5338 0x00792750 -> NextInt 0x007929a4 1 word ``` Every one of those edges is a direct `call` with a **single** incoming reference, so the attribution is not a guess: 0x00503200 has exactly one caller (0x0078cc70), which has exactly one caller (0x007d5150); 0x00792750 has two call sites, both inside 0x007d5150; and 0x007d5150 has exactly one caller, `DetectEncounters` at 0x007d860b. **The whole detection residual is inside one 744-byte function with one call site.** That is a far better handle than a family bound over two counts nobody can observe, and it makes the residual a tractable modelling target rather than a fitted constant. From its body: * it first builds a bitmask over the same `records` vector (`entry[0]->+0xf9 == 0` sets bit `entry[0]->+0x28`) and returns immediately if the mask is 0; * it then loops the **player vector** at `+0x54/+0x58`, skipping players whose mask bit is set or whose `+0xf9` is non-zero, builds a per-player candidate vector (0x00795870), and for each non-empty one calls 0x0078cc70 — **that is the `NextFloat`** — whose non-zero return leads on, and whose return value of **0x12 dispatches slot 17, `CreateRaidEncounter`** (§4's caveat); * the measured cost is 1 call and 1 word per site per turn on 8-player and 7-player saves alike, so only one player reaches the draw per turn on both — the per-player gate, not the loop bound, is what sets the count. That last observation is the one to hand lane Y: **the 2 words are not `2 × |players|` and not a constant by luck — they are one per player that passes a gate, and on both corpus saves exactly one player passes.** A model that assumes 2/turn will be right on our saves and wrong on a busier one, and now there is a named function to read to find out when. ### 6.2 The hook that measured nothing, reported as loudly as the ones that did (rule 15) `EncounterDetectProcessTeamRecordHook` compared nothing, because it never ran. Its coverage notes, its gate predicate, its contact/detector classifier and its byte-vs-dword cross-check on `+0xfc` are all **unexercised**. They are kept in the tree because the moment a save produces a non-empty record vector they become the check that matters — but nothing in this report rests on them, and the `fc_byte_vs_dword_disagreements` counter has never returned a value. Lane I's classification rules are therefore still exactly as verified as they were: from the instruction stream, and from nowhere else. --- ## 7. Coverage — what these runs did not touch * **Five End Turns on two saves.** Human `ref-turn2` turn 4 → 5 (once clean, three times in the perturbed run) and Zuul turn 23 → 25. That is a thin workload measured precisely. * **No trade routes, no spy programs, no manufactured workload for either.** §3.2 says what it would take. Until then, every statement about the eight tail draw sites is "they are not reached on a game with none of either", and the four callees' gates are located but not read. * **`ServerSpyManager` slot 13 (1,869 bytes) was not read.** Its loop and its gate are unexamined; it is grouped with slot 14 above only by manager, not by evidence. * **The probe counters cannot say which caller.** They record entries, not return addresses — a deliberate cost of being calling-convention-agnostic — and §4 shows that costing something real. * **The perturbation's mechanism is unknown**, and it is the largest open item this lane produced. A second, independent instance may already exist and be undetected in some other lane's build; the `z2-endturn.sav` → `b2124798470d85ea…` check is cheap enough that every future VM lane should run it. * **No `Guard` region is declared by any hook in this family**, the same deliberate gap lane Z reported: these hooks measure the generator and claim nothing about game state. `undeclared = 0` in these traces is vacuous and must be read that way. * **`probes=8` is proved neutral on one save and one turn.** It is not proved neutral in general, and the Zuul numbers in §3 and §5 inherit that limit — they were taken on a configuration validated against a Human turn, not a Zuul one. ## 8. Corrections to earlier findings * `tail-rng-ledger.md` §11.1: the caller list for `CreateRaidEncounter` is incomplete — 0x007d5150 dispatches slot 17 as well (§4). * `tail-rng-ledger.md` §11.1: "three quiet turns in a row is unremarkable" — it is a 1-in-720 event, and the measurement shows the rolls do succeed (§4). * `tail-rng-ledger.md` §11.3 and lane Y's residual table: the 2-word detection residual is **not** the `AssignContacts` family and is not blocked on the `+0xfc` gate; `EncounterDetect_Run` receives an empty vector, and the words are in 0x007d5150's subtree (§6). * Lane V2's per-slot notes: "never observed firing" is confirmed for the draws and **wrong as a statement about the callees** — all four run on every turn (§3). * `combat-done-tail.md` tier 4 / lane V2: the trade slot-13 loop is over the **fleets** vector, and a 23-fleet save still reaches no draw (§3.1). ## 9. Files * Predictions (pre-registered): `sots-engine/docs/H-probes.md`. * Traces: `verify/traces/h-probes-human-t5.jsonl.gz`, `h-probes-zuul-t24-25.jsonl.gz`, `h-probes-human-perturbed-t3-5.jsonl.gz`. * Reports: `verify/results/shim/probes/{human-t5,zuul-t24-25,human-t3-5-perturbed}.txt`; tool `tools/probe_report.py`. * Oracle pair: `verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav`. * Addresses: `ghidra/addresses.d/lane-h.json` (3 entries, all `mapped`). * Shim: `sots-engine/src/shim/hooks/probe_entry.{h,cpp}`, the `probes=` config key, and `EncounterDetectProcessTeamRecordHook` in `tail_rng.{h,cpp}`; configs `shim.cfg.hprobes*`, `shim.cfg.hp{0,4,8,9,10,11}`, `shim.cfg.hoff`. --- ## 10. Priority insert from lane AI1 — run before the VM was released Lane AI1 asked for its P1–P3 on the same VM session. Results are written up where they belong, in `findings/subsystems/ai-turn-logic.md` §8.1; in brief: * **P2 held — and it is the roadmap-level one.** `ref-turn2.sav` loaded in **two separate process launches** (pids 3400 and 2028, killed and relaunched between), shim at `hooks=off` so nothing is installed, one End Turn each: both autosaves byte-identical to each other and to the 2026-09-07 oracle. Byte-identity subsumes `ModCount` and `Summary.Checksum`. **The AI's turn is reproducible from a save alone**, so `backlog.md` §2's Rung B stays as written. Seven fresh processes across two saves agreed today in total, counting the `z2-endturn.sav` bisect runs of §2. * **P1 could not be answered as specified.** `session.log` after a full load + End Turn of a game with an AI player is 22 lines and contains no `RunAI:`, no `random seed` and no `AI Process Turn` line; the game's stdout carries the same 22 and nothing more. The level-2 messages are not emitted by this build at its default log level, so the "one log read, no build" route does not exist. A suggestive counter-indication to the all-zero-seed prediction is recorded there as a hypothesis, with the decisive test named (record the `this` pointer beside the return address in `draw_sites.cpp`). * **P3 not run** — no `ModCount` watchpoint was armed and no debugger session was held. ## 11. VM140 released Left at the **main menu**, verified by screenshot, build `recap-7584bad-20260908T0615Z`, `shim.cfg` = the recap trace config, `SavedGames\` back to the documented **8-file** set (`ref-turn2` still at (400,436), `zuul-turn5` at (400,465)) with the three autosaves carrying the oracle bytes `bb4fd9ac…` / `978041ac…` / `978041ac…`. Lane H left `C:\SOTS\shimdist-h` and `C:\SOTS\ui\h{deploy,release}.ps1` in place, plus `C:\SOTS\ui\h-oracle-{endturn,autosave}.sav` and `ai-run{1,2}.log`. **One VM note worth carrying forward.** The board's advice to screenshot before clicking is right but under-specified: the *intro* screens are bright white, so a brightness threshold fires on them and the click batch lands nowhere. The reliable detector is **redness in the main menu's Exit button** — crop `100x14+462+674`, require `mean.r > 0.2` and `mean.g < 0.15` (the menu reads 0.327 / 0.036; the white intro reads 1.0 / 1.0; a black screen reads 0 / 0). With that, every one of this lane's ten launches reached the menu on the first attempt, at t+48…54 s.