INPUT (17 visible rows) outgrew the fixed 440x330 paper: the 18px pitch
was sized before the D356 profile row, so a full page's last row landed
at y=335 (off the bottom of the screen) and rows past the silent 14-row
cap (Crouch mode, Reset to defaults, Bindings...) were unreachable from
the front screen. Sections now page at 13 rows (the largest that fits);
the vertical stepper crosses page boundaries in both directions and the
title carries a (p/N) marker. F10 overlay untouched (it scrolls its own
window). Headless verification (SkipIntro boot + scripted D-pad stepping
+ GE_FRONTNAVLOG): page 1 -> page 2 down and back up, all 17 rows
reachable. GE_FRONTOPTIONS_AUTO test hook added (diagnostic-only, same
convention as GE_FRONTOPTIONS_SECTION). Findings D406 + index; release
notes: options section list corrected to the shipped Input/Gameplay/HUD/
Graphics/Audio/Display and the D406 line added.
- Docs site: drop jekyll-theme-cayman for a hand-rolled _layouts/default.html
(masthead band + middot text nav + footer with last-updated stamp and legal
line; head re-plumbed with {% seo %}, viewport, favicon); head-custom.html
fully rewritten to the chosen design tokens - charcoal/off-white/amber,
Libre Baskerville headings over system sans, square corners, 1px borders,
flat (no gradients/shadows), mermaid wrapper re-skinned.
- docs/index.md: v0.4.0 status (native widescreen, aim system, rebinding,
crosshair, rumble, rebuilt overlay); Honest status replaced by the v0.4.0
release-notes known issues; body anchor-nav line; legal footer moved to
layout.
- security-and-fidelity-status.md: front matter so {% seo %} titles it.
- README.md: v0.4.0 truth pass (status, Working/Known issues, Deck preset
2x MSAA / 250% per code, background stats 981 commits / 283 findings,
comparison table, GEPD-default controls paragraph, earlier builds) plus
markdown-native motifs matching the site: top anchor-nav line, --- section
rules, last-updated footer stamp, license badge in the amber accent.
- release-notes: SFTP sideload mention (leftover from the Deck session).
The campaign has 20 playable missions (SP_LEVEL_MAX // 20 in
bondconstants.h; mission_folder_setup_entries in front.c has exactly 20
entries). The old '21' counted -level_54 (LEVELID_CUBA), which the front
end jumps to after the final mission for the extended cast display +
credits scroll (do_extended_cast_display / bondviewRenderCredits, D129) -
it is not a selectable mission.
- README, docs/index.md, .github/release-notes.md,
docs/security-and-fidelity-status.md: 21 -> 20 in current-status claims
- AGENTS.md phase status: same correction
- docs/dev/LEVEL-STATUS.md + LEVEL-PLAYTEST.md: relabel the level list;
counting note explains historical '21/21 PASS' sweep results = 21 stage
boots (20 missions + ending sequence)
- social-preview card: stat line 21 -> 20, PNG regenerated (1280x640)
Per the 2026-09-19 adversarial pre-ship review (findings.md M-192):
- d318watchdog.c: one-shot per OCCURRENCE, not per process -- re-arm after
600 ticks of signature absence so a Facility replay in the same session
(AllUnlocked) is still covered.
- model.c: RULE-2-SIGNOFF records for the retained M-183/M-185 and D311
defensive clamps (user-approved retention as the stability-maximizing
option; stale 'Threshold: 50.0' comment corrected to 10.0f; both marked
post-release removal candidates pending full cutscene re-verification).
- reference/mouse-injector/: removed from the public repo (GPLv2 source +
prebuilt discord-rpc.lib in an MIT project; never compiled here).
Provenance stub README remains; path gitignored for local re-vendoring.
AGENTS.md / GEPD-INPUT-PLAN.md / optionsoverlay.c refs updated.
- scratch/: untracked + gitignored (19 session logs/probe sources were
committed in the release window).
- findings-index.csv: D317 (and 5 other stale rows) now CLOSED -- added
CLOSED to gen_findings_index.py's verdict vocabulary.
- docs: D310 perf gauge (no change warranted), GE_DEBUG_UNLOCKALL noted in
security-and-fidelity-status.md, release notes gain the missing D318
watchdog bullet + FOV-clamp clarification.
Build verified: ./build-pc.sh ntsc-final clean; gen_findings_index.py
--check and gen_env_probes.py --check pass.
- GE-ENV-PROBES.md reconciled (18 new vars documented, GE_D243X3 tombstoned
after the M-170 permanent fix removed it) — CI gen_env_probes.py --check passes
- release notes gif alt text: ~32 s -> ~12 s (matches README/index captions)
- agentic-development.md stats regenerated for the month-one milestone
- docs/index.md status bumped to v0.3.0, honest-status synced with README
- .gitignore: local/ working-junk dir
- release-notes: drop the internal 'carried forward' meta paragraph; move the
D251 bottom-row duplication out of Known Issues into the F10 fix bullet
(no longer occurring per user live playtest of the D304 build)
- findings: close D251's last symptom; mark D304 live F10 playtest done;
de-stale D266/D271 'uncommitted' cells (landed in 532092c7); regen index CSV
- README: link the security & fidelity status page
- docs/security-and-fidelity-status.md: publish — re-verified for v0.3.0 on
2026-09-18 (bundle contents, no-networking source check, asset converter);
D271 now landed; D284 RNG history reworded as found-and-fixed pre-v0.3.0
Verified: build-pc.sh ntsc-final clean (243/243); tools_pc/verify.sh sweep
21/21 PASS on this tree (scratch/verify_sweep_v030_final.json).
Widens the written scope of AGENTS.md non-negotiable #2's ABI exception to
match established practice (runtime struct/union misreads, not just
ROM-serialized structs — the D209/D210/D255 pattern), states explicitly
that diagnosis into src/game is never restricted (only the fix is), and
defines a real three-bucket taxonomy + sign-off procedure (dev-process.md
§7) for the rare genuine-behavior-difference case, replacing the
undefined ad hoc "ask the user" gesture that's been referenced without
being defined across ~10 places in the docs.
Mirrors the same framing into CONTRIBUTING.md and the PR template for
external contributors. Extends porting-notes.md §A1 with D255 as a third
worked example and a tell-tale-signs checklist, explicitly distinguishing
it from D284 (same epistemic shape, different mechanism, no rule-2
question at all). Appends a ready-to-dispatch pass-10 brief to the D236
entry applying the new taxonomy to its actual next step.
Docs-only; no code/build changes. Per docs/dev/notes/RULE-2-CLARIFICATION-PLAN.md.
The 150 KB threshold assumed the §F region was table-only, but it also
holds ~92 KB of pre-ctx-opt D24–D33 bullet narrative plus the M-111/M-113
write-ups. The regression signal is fat ROWS re-accumulating (table is now
~55 KB), so the budget sits ~15 KB above today's 166 KB total.
Warn-only (non-fatal): findings.md §F table region > 150 KB means fat rows
are accumulating again (cell content belongs in ## Dxx sections; lookup path
is findings-index.csv); porting-notes.md > 120 KB. Lit until Bucket B of the
ctx-opt migration lands (~266 KB today). HANDOFF.md is gitignored so its
> 30 KB budget stays a local check.
Root-caused via a live Steam Deck session over SSH: sub_GAME_7F09BAC4
(vtxstore_fix_refs) read an active OBJ record's Model* through
((Model*)((ChrRecord*)prop->chr)->chrflags) - a union pun onto
ChrRecord.chrflags, which is typed CHRFLAG, a 4-byte enum. On 32-bit
N64 this is bit-exact (the enum and a pointer are both 4 bytes, so
casting the enum's value to Model* reproduces the real stored bytes).
On 64-bit PC, Model* is 8 bytes but the enum read is still only 4 -
the cast reads just the pointer's low 32 bits as an int and
zero-extends it back to a 64-bit address, discarding the real high
bits. This is the D3x pointer-width ABI class from AGENTS.md - a
struct field misread across the 32-to-64-bit pointer-width transition.
This was actually flagged as suspect (a) in the very first D255
investigation, then wrongly deprioritized in favor of a "destroy-path
race" theory once a register capture happened to show Rdi=(nil) and
looked like it confirmed a different, unrelated NULL-model theory. It
didn't: (nil) is exactly what a 32-bit-truncated read produces
whenever the real pointer's low 32 bits happen to be zero - a
coincidence that pointed two sessions' worth of investigation at the
wrong field. Proof: reading the same record through the correctly
64-bit-typed ObjectRecord.model field (this session's diagnostic
probe) was always a valid, non-NULL pointer - the object's model was
never actually missing.
Fix: read the pointer through ObjectRecord.model (the same union
memory, correctly typed) instead of through the mistyped
ChrRecord.chrflags alias, at both use sites in the function. Identical
bytes and identical result on N64; correct on 64-bit PC.
Verified live on the user's own Steam Deck hardware over SSH, running
their own prepared ROM assets: held through the entire Facility level
including repeated terminal destruction (the most consistent prior
repro) and through Caverns' radio-objective terminals (the second
confirmed level), zero crashes. Windows rebuild + full 21-level
verify.sh sweep also clean.
The separate audio-thread crash (D285, caught during this same live
session) is unrelated and unaffected by this fix.
Further Steam Deck testing on the published v0.2.1 release reconciles
the two competing D255 repro theories: the trigger is an object dying
via explosion, not terminal activation. Confirmed on four levels:
- Facility: every terminal crashes consistently when destroyed
- Caverns: terminals near the radio objective crash when they explode
- Archives: a similar crash when a guard's grenade detonates (no
terminal involved at all - narrows the real trigger to "object dies
via explosion damage" in general)
- Frigate: negative control - shooting a room of terminals there did
not reproduce it, so it isn't simply "any destructible OBJ record
exploding" either
Updated the D255 finding with a static lead for the next session (the
objFree destroy path never explicitly nulls ObjectRecord.model, so the
NULL likely comes from a freed-slot-reuse or list-modify-during-iterate
race rather than a direct field write - untested, needs a live Deck
capture with the GE_D255 probes already in tree). Updated
.github/release-notes.md's D255 bullet to match.
Batched pass across README/release-notes/Pages (one commit instead of the
scattered per-fix pushes earlier this session, per new commit-cadence
preference):
- New known issues surfaced that weren't documented anywhere: no real
widescreen (16:9 stretches 4:3 rather than expanding FOV), no controller
rebinding UI / macOS-ARM builds, culling/LOD dropout on Streets/Egyptian
(D249), gunshot SFX oddities under sustained fire (D240/D241).
- Fixed a stale claim (in three places) that a wrong-sounding-instrument bug
was still open; it was fixed in v0.2.0 (D230).
- Softened the D259 "All unlocked" fix to reflect reality: still breaks
audio/aim if enabled before any save exists; flagged experimental
(new finding D281).
- Corrected D255's repro to the precise, deterministic trigger found this
session (Facility's door-opening computer terminals, Linux-wide not
Deck-specific) instead of the vaguer original combat-crash description.
- Logged D282 (front-end menus need right stick not left on gamepad) and
D283 (Steam Deck first-run preset skipped by an early Desktop Mode
launch) with user-facing workarounds.
- Refined D263 (Ourumov/Trevelyan) internally with a position-sensitivity
caveat; no public bullet since it doesn't block progression.
- Added pre-1.0 framing and a new README Roadmap section (known-issues
cleanup, PAL/JP, real widescreen, rebinding, macOS/ARM, LAN multiplayer
explicitly "under consideration, no ETA", plus an explicit "not currently
planned" list) across README/release-notes/Pages.
- Added an AI-development disclosure (agentic development as a genuine
study of the process, not just a port) across the same three docs.
- Pinned the release-notes hero GIF to the v0.2.1 tag instead of v0.2.0.
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
User spot-check of the v0.2.1 draft found that enabling Skip intro breaks
audio. It was already off by default (D216); removing the menu row so
players can't enable it until the audio interaction is root-caused. The
config var and lv.c hook are untouched (dead unless an existing ini already
has it set).
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
User playtest reports against v0.2.0:
- D259: geEepromPatchAllCheats left an all-zero fresh save at volume 0
(silence) instead of seeding max volume like fileResetSave would.
- D260: swap wheel-up/wheel-down to match the PC wheel-menu's on-screen
order (was inverted vs. user expectation).
- D261: watch-menu stick-held navigation had no repeat past the smooth-scroll
band, so holding W/S or a pegged stick crawled one item per tap; added a
bounded auto-repeat.
- D263: GE_INPUTSCRIPT stick tokens were applied after the stick was already
written out and silently discarded (test-harness fix, kept from the D263
Facility/Ourumov investigation, which itself found no port bug).
- D264: watch item-preview ModelRenderData template read across two globals
that no longer sit adjacent on PC, zeroing renderdata.flags and gating all
geometry emission in the preview's subdraw pass.
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
- Drop-in ROM wording everywhere (README quick start, Pages landing,
release notes, bundled README): first launch auto-generates the
derived assets via the frozen converter; no Python/toolchain needed.
PAL/JP marked not-yet-supported with issue #85 pointers.
- building.md: Linux build is CI-compiled and ships in the bundle
(was 'never built or run on Linux'); bundle note now describes the
frozen ge007-convert auto-spawn; PAL/JP sidecar caveat (D258).
- Status rewording across README/Pages/release notes: drop the
'first open-source port to our knowledge' claim; cutscene glitch
description aligned to 'mostly with James Bond'; index.md status
no longer contradicts the open D255 Deck crash.
- Em-dash cleanup across all GitHub Pages docs (index, building,
internals, porting-notes, dev-process, agentic-development,
release notes); internal dev logs untouched.
* Drop-in ROM: first-run sidecar conversion, no Python (issue #6, Part A + interim)
The release bundle now turns a dropped-in NTSC-U ROM into a playable game
with zero tooling: when data/pcmodels-* / data/pccg-* are missing at boot,
the engine spawns the frozen prepare-assets converter shipped in the bundle
(prepare-assets/ge007-convert[.exe], PyInstaller --onefile), waits for it,
and re-checks before continuing (port/src/romconvert.c, called from
romdataInit after ROM validation). Failure aborts boot with a message
instead of the later D179-class crash.
- prepare-assets.py: runs in-process via runpy when frozen (no user Python
to exec); NTSC-U-only gate with a clear message for PAL/JP ROMs; d88's
sibling import needs an explicit sys.path entry (run_path does not set
one). Verified: frozen exe output byte-identical to the unfrozen path and
to the shipped sidecars.
- CI (linux + windows): PyInstaller freeze step before bundling
(ensurepip / --break-system-packages for MSYS2's pip-less python).
- bundle scripts: copy ge007-convert into prepare-assets/ (hard-fail if not
frozen), size guard 60 -> 120 MB.
- Release notes: US-only drop-in instructions, no Python required.
- D258: PAL/JP filelist naming defects documented; multi-region deferred to
the Part B follow-up.
* CI: install pip via pacman on Windows (MSYS2 ensurepip is disabled); regen findings index for D258
The one broken bass instrument on Control/Cavern/Runway is a plain
byte-order bug in a corner the earlier passes never looked at. The
instrument bank's only two AL_RAW16_WAVE wavetables (instruments 2 and
51, sharing base 0x2118 / len 790) decode as a clean ~110 Hz soft-attack
bass sample when read big-endian s16 -- which is how the N64 CPU reads
them. The PC mixer's resampler consumes host-native (little-endian) s16,
so every sample arrives byte-swapped: deterministic per-instrument
corruption, exactly the M-106 repro. ADPCM nibble data is byte-order
agnostic, which is why every other instrument sounds right.
romdataFinishCartMap() now walks both bank trees (same ALBankFile layout
as romdataFixupAudioBank) and bswap16s each RAW16 [tbl+base,+len) range
in place in the cart map -- where alBnkfNew() points the mixer at it
directly. Shared ranges are deduped (swapping twice undoes the swap);
all reads bounds-checked. D55 in-place-fixup idiom; port-only, no game
logic touched.
Startup log: 'romdataInit: D230 byte-swapped 1 RAW16 wavetable range(s)
in place'. Owes a by-ear pass on the three repro tracks.
findings.md D230 -> FIXED with full root cause; release-notes.md drops
the music known-issue bullet.
* Release notes: finalize for v0.2.0 final (CI re-stamps the body on tag cut)
- Drop '(pre-release)' from the title; <version> stamps to 0.2.0, so the
Downloads/Verify tables already name the final assets correctly.
- Blockquote rewritten: no more 'cut is for playtesting'. Claims what is true
(21 missions load and run crash-free) without claiming an end-to-end
completable campaign; adds the first-public-release note, hedged with
'to our knowledge'.
- New What's-new bullets: Everything-unlocked-by-default (D257, ON by default,
F10 opt-out, no active cheats) and the Steam Deck first-run preset + the two
new F10 rows (No hit flash / Skip intro).
- Known issues unchanged — all eight walked with the user and confirmed still
open; wording already matches the latest reports.
- Preview montage GIF at the top of the body (release pages can't play mp4);
raw URL pinned to the v0.2.0 tag, so it resolves once PR #80 has merged and
the tag is cut.
* Release notes: campaign-completed claim + D255 register-capture diagnosis
- Blockquote: full campaign playtested end to end (all 21 missions, Agent
difficulty) per the v0.2.0-pre PC playtest
- D255 bullet: updated from 'narrowed to a stale model reference' to the
pinned diagnosis (NULL ObjectRecord.model on an active record during
mid-firefight destruction; repros on Bunker + Frigate)
* README: 32 s gameplay preview montage (mp4) + preview-video runbook
* Prune: drop superseded goldeneye-demo.gif (replaced by gh-preview montage)
* Docs: v0.2.0 finalization pass — final wording, aligned known issues, Deck-verified line, social preview card
- README + pages site: 'v0.2.0 (pre-release)' -> v0.2.0 throughout; the
stale 'not yet verified on real Deck hardware' line is replaced with the
playtest fact (D253 fixed, D255 still open).
- Known-issues lists in README + site now match the release notes one-for-one
(adds the F10 bottom-row duplicate D251 and the intermittent Deck SIGSEGV
D255; D243/D230/D252/D245/D246/D75 carry their finding labels).
- Working list mentions AllUnlocked default-ON (D257) with the F10 opt-out.
- First-public-release note added to the README hero and site status line,
hedged with 'to our knowledge'.
- social-preview.png (1280x640 GitHub link card) composed from a gameplay
frame of the preview montage; scratchpad/make-social-preview.py is the
local recipe (gitignored, like make-preview.py).
- Commit docs/img/shots/manifest.csv (gallery -> source recording map);
gitignore the regenerable CONTACT-SHEET.jpg; drop scratch-gdb-cmds.txt.
* Social preview: regenerate card from updated briefing HTML (v0.2.0 final wording)
The PNG committed earlier was a screenshot of the rendered site (stale
wording, video element unrendered). The canonical source is
.github/social-preview.html per .github/social-preview.md; update its
Status/Statistics lines to the v0.2.0 final state and re-render:
- Status: RELEASED v0.2.0 — WINDOWS · LINUX · STEAM DECK
- All 21 solo missions — full campaign completed in playtesting
- ~22k lines C/C++ · ~10k Python (port layer + tools)
- ~250 bugs root-caused · two coding agents, one human, part-time
Verified by pixel-diff against the previous render: only the four text
bands changed; title/fonts/layout identical. Drop the unreferenced
root-level social-preview.png.
* docs: refresh social preview + status for v0.2.0 playtest results
- Social card: 'RELEASED v0.2.0', 21/21 missions completed in playtesting,
~250 bugs fixed, two agents + one human; re-rendered the PNG from the
canonical .github/social-preview.html source (pixel-diff vs the old render
shows only the four edited text bands changed)
- README Status: campaign now claimed as completable end to end (user
playtest: full campaign, Agent difficulty, PC); Working list matches
- findings: D255 updated with the Deck register capture (Rdi=nil -> NULL
ObjectRecord.model, destroy-path ordering case confirmed; truncation ruled
out) and the rule-#2 sign-off question for a guard; D254 field-verified
* docs: regenerate findings index (D254/D255 status updates)
The release job only set --notes-file when creating the draft; existing
drafts got 'upload --clobber' (assets only), so the body stayed frozen at
the first cut's notes. Compute the stamped notes unconditionally and add
'gh release edit --notes-file' to the update path.
D256 (Linux saves/config now persist from any launch dir) is in this cut
and Deck-verified; the D255 line reflects the narrowed diagnosis and the
register-capturing crash handler. Drop the 'no known crashes' claim from
the pre-release blurb since D255 is open.
The POSIX handler re-entered on SIGABRT (sysFatalError -> abort) and
truncated ge007.crash.log, so field crashes came back as bare abort
records with no faulting PC or registers. Guard against re-entry,
terminate directly with _exit(), and dump si_addr + the general register
set from the ucontext — a Deck crash now names the bad pointer on its
face.
Also documents D255 (new SIGSEGV in vtxstore's active-object walk during
a Facility firefight on the Deck; disassembly shows the fault is a
32-bit-zero-extended read of ObjectRecord.model, misnamed ChrRecord.
chrflags by the decompiler) and updates the release notes' Deck status.
- D197 (Silo face wrap): closed per user playtest, dropped
- D75: reworded — Nintendo logo still broken, Rareware logo filtering off;
mission-complete/mode-select screens have no 3D models (old framing wrong)
- F10 ghost row: now deterministic bottom-row duplicate of the selected item
(D251 addendum); earlier intermittent 4K framing superseded
stanCheckLinkedSpecialTile was decompiled with an all-s32 middle-arg
prototype while its callers pass it through the standTileLocusCallback_B_t
typedef (f32 f32 f32). On x86-64 SysV the int/float class split puts the
6th arg (outFlags/record) in %rdx per the typedef but %r9 per the
definition, so the FORCECROUCH/LADDER store hit a stale scratch register.
MIPS o32 passes both through GPRs a0-a5 identically, which is why the
mismatch was invisible on N64 and in the decompilation.
ABI-class edit (AGENTS.md #2 narrow exception): body never uses the three
middle params; N64 codegen unaffected. Verified under gdb on WSL:
pre-fix SIGSEGV at stan.c:2441 with r9=0x3 and a valid record in the
caller frame; post-fix the same walk route completes clean.
- README: manual Contents TOC dropped (GitHub auto-generates); Download
table moved to the top with Quick start under it; Status compressed to
a Working paragraph + tight known-issue list (D-numbers live in the
release notes / finding log now); How-it-works tightened.
- Pages (docs/index.md): demo gif is the hero; download table up top;
new How-it-works and Honest-status sections; legal footer; the 19-shot
playtest gallery unchanged.
- Options overlay is now gamepad-driven: README Steam Deck section, the
bundle README (new @DECK@ block on Linux, controller column added to
the controls table), and the release-notes template all document
Select / D-pad / A / B / Start instead of a Steam key-mapping workaround.
Two defects found in the local review of the unpushed v0.2.0-pre work:
1. input.c: padShoulderPrev was only updated outside menu mode, so a
shoulder held across the menu->game transition fired a spurious
weapon cycle on the first gameplay poll. Edge state is now tracked
every poll; emission stays suppressed in menus where A means select.
2. ci.yml release job: the notes template substitutes <version> with the
raw tag (v0.2.0-pre), but bundle-{linux,win}.sh strip the leading v
when building asset names -- the published notes would have linked
goldeneye-pc-port-v0.2.0-pre-* assets that don't exist. Substitute
the stripped form (NOTES_VERSION) instead.
Steam Deck target. LS move/strafe + RS look + RT fire + LT aim were
already in place (D194/D238 naturalPitchMode); this completes the
scheme with shoulder weapon cycling per the console re-releases:
RB rising edge = next weapon, LB rising edge = previous weapon.
Cycling rides the game's own A-line formulas (bondview2.c
weaponForwardOffset/weaponBackOffset): a fresh A edge cycles forward;
A+Z presented on the same tick cycles backward -- the identical trick
the mouse-wheel path uses (D223), which also guarantees no stray shot
(triggerOn is gated off while invButtons is held). Edges are emitted
for exactly one poll so holding a shoulder cannot latch A and block
firing; both are suppressed in menu mode where A means select.
A/X = action/use, B/Y = crouch/cancel (unchanged); LB no longer maps
to lean. README controls table + release notes updated.
User playtest report: bullet-impact sparks and lingering smoke/explosion
residue drift through the full hue wheel over time instead of holding their
intended grey/orange palette. Logged in findings §F (D252) with this
session's mechanism work: particle colour comes entirely from the 15
distinct IMAGE_SMOKE_N x IMAGE_FIRE_N (RGBA16) pairs the particles age
through (assets/oddtextures.c records, white default vertex); the M-82/D172
tile-offset fix and the 5551 decode are both verified correct for this path;
prime suspect is import_texture_rgba16 deriving width/height from the tile
line size (32x7 instead of 16x14 for the 16px-wide fire tiles), which would
scramble each aging stage into a different wrong hue -- exactly the observed
slow rainbow sweep. Per user instruction: documented only, ships as a known
issue (README + release notes + GRAPHICS-BACKLOG); index csv regenerated.
bundle-linux.sh now copies the exact libSDL2 the exe was linked against next
to it and sets the rpath to $ORIGIN via patchelf, with an ldd assertion that
the bundled copy is the one resolved. zlib/libGL remain system deps
(preinstalled on desktop distros and SteamOS). CI installs patchelf; the
generated in-bundle README gains a Steam Deck sideload note and the SDL2
zlib-license copyright file is carried when the distro ships one.
Also refreshes the release-facing docs that ship with / describe the cut:
- .github/release-notes.md: v0.1.0-alpha body -> <version> pre-release body
(what's new + known issues incl. D243/D230/D245/D246/D197/D75, the D251
4K ghost row, and 'not yet verified on real Deck hardware')
- tools_pc/dist/README.md.in: alpha claims (no audio / black sky / US-only)
-> pre-release status; ROM table now lists all three regions (the asset
step auto-detects by SHA-1)
Previously: self-hosted for push/workflow_dispatch, windows-latest for every
pull_request (including the user's own same-repo branches). Per standing
preference (2026-09-12): route all Windows builds to the self-hosted box for
now, except when cutting a release (tag push) -- that stays on a clean
GitHub-hosted runner for the artifact that actually ships. Fork PRs also
stay on windows-latest regardless (untrusted code must never reach a box
that will later hold a ROM, docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md §G2).
Adds a `windows_selfhosted` output on the `changes` job (computed once) and
threads it through windows-build's runs-on, the msys2/setup-msys2
release/update/install flags, and the self-hosted dependency check --
replacing every prior `github.event_name == 'pull_request'` check.
* ci: route windows-build to self-hosted runner (G2); land ROM-gated sweep workflow (G1)
G2: windows-build now runs on the self-hosted box (corpyvt6agent01) for
push/workflow_dispatch, using local ccache instead of the actions/cache
round-trip and skipping msys2/setup-msys2's install once packages are
already present. pull_request always stays on windows-latest — fork PRs
must never reach a box that will hold a ROM.
G1: adds .github/workflows/selfhosted.yml, a dispatch-only workflow that
builds + runs the 21-level headless sweep (tools_pc/verify.sh sweep --json)
against a runner-cached ROM. Inert until manually dispatched.
Still pending (manual, physical steps on the box — not done by this
commit, see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md rollout table):
- register corpyvt6agent01 as a runner against this repo
- confirm MSYS2 at C:\msys64 with the required mingw64 package set
- one-time ROM seed into {runner_home}/_rom-seed/
- first workflow_dispatch sweep + parity check against a local run
- flip G1 from workflow_dispatch to push:[main] once green
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): remove invalid concurrency.max-parallel key
Found by testing: dispatching the workflow failed with a hard parse
error (HTTP 422, "Unexpected value 'max-parallel'"). GitHub Actions'
workflow-level `concurrency` block only supports `group` and
`cancel-in-progress` — `max-parallel` is a job `strategy:` key for
matrix jobs, not a concurrency-block key. Serialization was already
achieved via group + cancel-in-progress:false; the stray key just
blocked every dispatch.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): register the msys2 shell wrapper (setup-msys2 step)
Found by testing: the first fixed dispatch failed every msys2-shell step
with "msys2: command not found". Unlike ci.yml's windows-build, this
workflow used `shell: msys2 {0}` without ever running msys2/setup-msys2,
which is what registers that shell alias for the runner — MSYS2 being
pre-installed on the box doesn't help if Actions doesn't know the shell
exists. Adds the same setup-msys2 step ci.yml uses, with update:false
since the packages are expected already present (fast no-op per plan).
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: stop reinstalling MSYS2 per run on the self-hosted box
The self-hosted paths (selfhosted.yml; ci.yml windows-build's push/
dispatch route) no longer call msys2/setup-msys2 or pacman -S at all.
MSYS2 + the mingw64 package set are now assumed hand-maintained on
corpyvt6agent01 (docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md), invoked by
calling its bash directly (`C:\msys64\usr\bin\bash.exe -leo pipefail
"{0}"`, the same invocation setup-msys2's generated wrapper uses) with
MSYSTEM=MINGW64 so /etc/profile still puts mingw64/bin on PATH.
Adds tools_pc/check-selfhosted-deps.sh: a fast, install-free check that
exits 0 immediately when the required packages (toolchain, cmake,
SDL2, zlib, python, ccache, zip, unzip) are present, or fails with the
exact `pacman -S --needed ...` line to run otherwise — so a missing
package on the box surfaces immediately with a clear fix, instead of
as a confusing failure deeper in the build.
ci.yml's windows-latest (PR) path is unchanged: ephemeral runners still
get a full msys2/setup-msys2 install every time, since there's nothing
to persist there.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix: use msys2/setup-msys2 release:false instead of a raw bash-path shell
The previous attempt (invoking C:\msys64\usr\bin\bash.exe directly as a
custom `shell:` command) failed on the actual runner with "Second path
fragment must not be a drive or UNC name" — a real limitation, not a
typo; it failed identically in both files, including the one with a
static (non-templated) shell string, so it wasn't the ${{ }} expression
either.
Root-caused by reading msys2/setup-msys2's actual source: `release:
false` makes it reuse the existing C:\msys64 as-is (hardcoded path in
that branch, confirmed in main.js) instead of downloading+installing a
fresh copy, and — verified in the same source — omitting `update` and
`install` skips every pacman call entirely (just one `uname -a` + a
pacman.conf sed tweak, no network). This is the officially documented
mechanism for self-hosted runners with a pre-existing MSYS2 install
(see the action's README, "release" section).
ci.yml's windows-build now runs setup-msys2 unconditionally but with
release/update/install all keyed off github.event_name: full install on
windows-latest (PR, ephemeral), reuse-only on the self-hosted box
(push/dispatch). selfhosted.yml always uses release:false. Both then
run tools_pc/check-selfhosted-deps.sh to verify the box's hand-
maintained packages are actually present.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): cp -r for the ROM sidecar directories
Found by testing with the ROM seed actually in place: pcmodels-ntsc-final
and pccg-ntsc-final are directories (not single files, unlike
ge007.ntsc-final.z64), so a plain `cp` failed with "omitting directory"
and left the seed step failing on every run even with the seed populated.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim
The self-hosted runs were dying with 'msys2: command not found' because the
msys2/setup-msys2 action's shell shim was never registered on the box. Replace
the dependency on that shim with a small committed launcher (tools_pc/ci-shell.cmd)
that headlessly reproduces the local 'MSYS2 MINGW64' terminal:
- MSYSTEM=MINGW64 + /mingw64/bin on PATH
- NON-login bash, so it keeps the workspace cwd (a login shell cd's to $HOME
and would break every relative path in the workflow)
- -e -o pipefail for GitHub-style error propagation
Both workflows now set defaults.run.shell to C:\msys64\ci-shell.cmd {0}; a
bootstrap step (shell: cmd) refreshes the launcher from the repo into C:\msys64
each run. selfhosted.yml drops setup-msys2 entirely; ci.yml keeps it only for
the ephemeral windows-latest PR path (which installs MSYS2 at C:\msys64).
.gitattributes: force CRLF for *.cmd so cmd.exe parses the launcher correctly.
* Revert "ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim"
This reverts commit a976b2589c.
* docs(D244): log self-hosted-runner link failure — undefined reference to objectiveGetStatus_WEAK
First real build attempt on the reinstalled corpyvt6agent01 box (run
34707798707) fails at link with an unresolved #pragma weak alias. A local
repro of the identical alias pattern links fine on current MSYS2
(gcc 16.2.0/binutils 2.47); the box reports gcc 16.1.0 — likely a stale
toolchain, consistent with rollout step 2's earlier package-hygiene gap.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): surface run.log tail on NO-FRAMES instead of silently discarding it
sweep mode's per-level run.log (the game process's own stdout/stderr, where
an SDL/GL init failure would show up) was captured into a mktemp capdir but
unconditionally rm -rf'd with no diagnostic ever printed for the NO-FRAMES
(no-crash) case -- every level failed this way on the self-hosted runner's
first real sweep attempt with zero clue why.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(selfhosted.yml): stop deleting the ROM seed dir -- cache save silently fails on this box
actions/cache/save has been failing to persist the ROM cache entry on
corpyvt6agent01 (WARNING annotation, tar.exe exit code 2) but doesn't fail
the step -- by design, a cache-save failure is non-fatal, so the workflow
carried on and the now-unconditional "Delete one-time seed dir" step wiped
the only copy of the ROM every single run. Every dispatch since has failed
at "Seed ROM cache" with an empty seed dir and an empty cache -- exactly
the user-reported "it wipes out the ROM data each run".
Fix: don't delete the seed dir. It's cheap disk (three files, a few hundred
MB) and the existing restore-then-seed-if-missing logic already treats it as
the fallback source whenever the cache comes back empty, so leaving it in
place is safe and makes the workflow tolerant of the cache-save bug until
that's root-caused separately.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): sweep --json never actually produced a verdict array; fix NO-FRAMES diagnostic clipping it further
Two bugs, found together while chasing the self-hosted runner's first real
sweep attempt:
1. sweep mode's `out=$(verify_level ...); echo "$out" | head -1` always
discarded emit_verdict's second (--json) output line -- RESULTS_JSON was
declared for assembling a verdict array but never populated or printed,
so `sweep --json` has never produced anything but `[]` for the
workflow's "Extract JSON verdict" step. Now builds and prints a real
`[{...}, {...}]` array before the RC exit.
2. My own D244-session addition (printing a NO-FRAMES level's run.log tail)
wrote to stdout, which the same `out=$(...)/head -1` capture turned into
the *new* first line, clipping the real verdict summary. Moved to stderr,
which bypasses that capture.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* docs: regenerate findings-index.csv for D244
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* docs(M-102): fix findings-index false-positive, release <version> bug, land parked site cleanup
Docs triage pass (docs/dev/notes/PROMPT-docs-triage-session.md).
- gen_findings_index.py: D194's row was silently bucketed FIXED (an open
design item) because it had no distinct status cell, so the generator's
fallback keyword search matched "fixed" somewhere inside 6,500 chars of
narrative -- the same misleading-index failure mode that cost a session
on D227 (see docs/dev/findings.md SS H "D227 -- M-98"/"M-99"). Gave D194
a real status cell. Also extended LEAD_RE/bucket() with 5 verdict tokens
already in use in the doc (ANALYZED, GUARDED, DIAGNOSTIC SHIPPED,
ROOT-CAUSED, NOT A BUG) and added a leading verdict word to 4 more rows
that had a status cell but no recognized token -- fixes all 10 rows the
generator previously bucketed "?" (D175 D174 D173 D220 D160 D133 D136
D156 D153, plus D194). findings-index.csv regenerated; --check passes;
diff reviewed, only these 10 rows moved.
- ci.yml: release-notes.md's <version> placeholders were never substituted
before `gh release create --notes-file`, so every published release body
(including the live v0.1.0 release) ships the literal text. Now sed'd to
the tag first.
- Landed docs/dev/notes/UNLANDED-docs-site-cleanup-steps3-5.{md,patch}
(parked M-101, never pushed, branch deleted): deletes the orphaned
docs/StructureGuide.md (0 tracked references), adds title/description
frontmatter to 5 published Pages docs, rewrites docs/index.md's stale
"Phase 2 of 4 / no audio yet / 19-of-21" status text to match the
current README. The 2 files whose hunks no longer applied via `git am`
(docs/index.md, docs/dev/agentic-development.md) were redone by hand
against current main.
- docs/BACKLOG.md (local, gitignored): retargeted a dead
docs/PORT-LEARNINGS.md reference to docs/porting-notes.md.
Larger triage items (findings.md monster-row convention, full
status-accuracy sweep, duplicate-defect clustering, dev-notes plan
triage, HANDOFF rotation policy) scoped but not started -- see
docs/dev/notes/FOLLOWUP-docs-triage-session.md (gitignored).
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
* docs/index.md: fix stale in-level-music claim before merge
This PR predates the M-102c/M-105 correction (already landed in the
README via PR #55): in-level music is NOT absent, it plays with some
tracks having wrong-sounding instruments / occasional garbling (D77
fixed, D230 filed for the residual). This branch's own rewritten Status
paragraph still said music 'doesn't yet' play -- fixing it to match
before merge so main doesn't briefly regress to a stale claim this
session already corrected elsewhere.
CMakeLists.txt already opt-in enables CMAKE_{C,CXX}_COMPILER_LAUNCHER=
ccache when found (b49907de); CI just never installed ccache or persisted
its cache dir across runs. Wires actions/cache + ccache install into both
linux-build and windows-build, keyed on CMakeLists.txt's hash. Game
sources are frozen, so this should give a near-total hit rate after the
first run on each job.
YAML-validated; the actual hit-rate payoff needs a real CI run (cold, then
warm) to confirm -- not verifiable locally.
Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
Static/tooling slice of docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md — no build or
runtime environment needed to verify any of it.
- Step 6 / N7: add a VERIFY: line to the investigation-brief template
(docs/dev-process.md) and the CLAUDE.md dispatch-preflight checklist — the
brief must now name the exact probe/verify invocation that proves the task
done, or state that runtime verification is impossible here.
- Step 4 / N2: tools_pc/README.md — indexes the ~60 scripts with a
living vs artifact marker so closed-finding artifacts can be archived.
- Step 4 / R8: tools_pc/gen_findings_index.py -> docs/dev/findings-index.csv
(label, bucket, one_liner, status_raw) — grep-before-you-read aid for the
~200 KB finding log. Heuristic parser of the §F/§H tables.
- Step 4 / R9: tools_pc/gen_env_probes.py — re-greps live getenv("GE_*")
sites and reports NEW/GONE drift vs docs/dev/GE-ENV-PROBES.md (keeps the
curated prose, only checks the site map). Doc reconciled: 7 new probes
added, GE_D54 kept as a tombstone.
- CI: validate job now runs both generators with --check.
Step 1 / N1 (HANDOFF preflight) applied in the local-only docs/HANDOFF.md.
Step 2 (verify.sh) and the HANDOFF/HANDOFF-ARCHIVE trim are left for a
session with a build environment.
Update the public-facing docs to the M-49 state (v0.1.0 alpha, first
full-campaign playtest: 19/21 missions completable, D191 crashes Bunker ii
+ Statue, D193 AI pacing is the top flaw).
- README: lead with what it is + status, research angle second; new
Quick start section; Status/Download rewritten for the alpha + dual
platform; "By the numbers" refreshed (~3 weeks, ~320 commits, ~190
findings, ~49 sessions, 69 PORT files).
- .github/release-notes.md: v0.1.0 title + known-issues rewrite.
- .github/social-preview.html: status/stat lines (regen + re-upload the
PNG separately).
- docs/index.md, docs/dev/LEVEL-STATUS.md: M-49 status.
- docs/dev/findings.md, docs/dev/GRAPHICS-BACKLOG.md: D191-D197 +
D148/D170/D173 M-49 escalations.
Hygiene:
- Fix the `&` literal in internals.md H1.
- Repoint two ../README.md links that 404 on the Pages site.
- Standardise British -> American spelling across the public docs
(behaviour->behavior, colour->color, catalogue->catalog,
centre->center, etc.). Deep internal logs left untouched.
- .gitignore: document the local-notes convention (docs/HANDOFF.md,
docs/BACKLOG.md, and a new docs/dev/notes/ drop for roadmaps and
session plans) so it survives a fresh clone.