Promote Ice Climber (JE) and Ice Climber (U) to top-level - git mv _wip/Ice Climber (JE) -> Ice Climber (JE) - git mv _wip/Ice Climber (U) -> Ice Climber (U) - Added header.s / chr.s (one-line .incbin each) - Added HEADER/PRG/CHR linker.cfg with proper memory layout - Added copilot/rom.nes symlink to original ROM - Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes) - Updated _tools paths from ../../../_tools/ to ../../_tools/ Both projects: Pass 1/2/3 complete, byte-identical at whole-ROM level (verified). Root OVERVIEW.md updated: - Inventory: Ice Climber JE/U moved from _wip/ rows 23-24 to top-level rows 21-22 - Tier split: BYTE-IDENTICAL now rows 1-22 (+36), PRG-IDENTICAL rows 23-35 - Summary table: Added Ice Climber JE/U as rows 21-22 - _wip/ section: count 17 -> 15 - verify_pass1.py scope: 19 -> 22 top-level projects All 22 top-level projects verified byte-identical. Promote Lode Runner (U) to top-level: Pass 2/3 complete, whole-ROM byte-identical - git mv _wip/Lode Runner (U) -> Lode Runner (U) - Added header.s / chr.s (one-line .incbin each) - Added HEADER/PRG/CHR linker.cfg with proper memory layout - Added copilot/rom.nes symlink to original ROM - Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes) Both projects: Pass 2/3 complete, byte-identical at whole-ROM level (verified). Root OVERVIEW.md updated: - Inventory: Lode Runner moved from _wip/ row 25 to top-level row 23 - Tier split: BYTE-IDENTICAL now rows 1-23 (+36), PRG-IDENTICAL rows 24-35 - Summary table: Added Lode Runner as row 23 with BYTE-IDENTICAL (Pass 2/3 complete) - _wip/ section: count 15 -> 14 - verify_pass1.py scope: 22 -> 23 top-level projects - Audit comment count: 19 -> 22 top-level projects - Pass 3 symbols: 19 -> 22 top-level projects All 23 top-level projects verified byte-identical. TODO.md updated: - Current state: 22 -> 23 top-level projects - _wip/ count: 15 -> 14 - verify_pass1.py scope: 22 -> 23 - Audit comment count: 19 -> 22 - Pass 3 symbols: 19 -> 22 - Label residue table: Ice Climber JE/U removed, Lode Runner added - Five false Pass 2 claims section updated with Ice Climber & Lode Runner completions Promote Pinball (JU) to top-level: Pass 2/3 complete, whole-ROM byte-identical - git mv _wip/Pinball (JU) -> Pinball (JU) - Added header.s / chr.s (one-line .incbin each) - Added HEADER/PRG/CHR linker.cfg with proper memory layout - Added copilot/rom.nes symlink to original ROM - Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes) Both projects: Pass 2/3 complete, byte-identical at whole-ROM level (verified). Root OVERVIEW.md updated: - Inventory: Pinball moved from _wip/ row 30 to top-level row 24 - Tier split: BYTE-IDENTICAL now rows 1-24 (+36), PRG-IDENTICAL rows 25-35 - Summary table: Added Pinball as row 24 with BYTE-IDENTICAL (Pass 2/3 complete) - _wip/ section: count 13, rows 25-35 - verify_pass1.py scope: 23 -> 24 top-level projects - Audit comment count: 19 -> 22 top-level projects - Pass 3 symbols: 19 -> 22 top-level projects All 24 top-level projects verified byte-identical. TODO.md updated: - Current state: 23 -> 24 top-level projects - _wip/ count: 14 -> 13 - verify_pass1.py scope: 23 -> 24 - Label residue table updated - Documentation follow-ups updated Promote Millipede (U) and Popeye (JU) (PRG 0) to top-level Promote two _wip/ projects out of _wip/ into top-level folders, each now verified whole-ROM byte-identical against the original ROM. - Millipede (U): moves all copilot files up, adds header.s, chr.s, linker.cfg with PRG+HEADER+CHR areas, rom.nes copy, and switches the Makefile from PRG-only cmp to whole-ROM cmp. Makefile _tools depth adjusted (../../../../_tools/ -> ../../_tools/). - Popeye (JU) (PRG 0): same promotion treatment. - AGENTS.md: update layout notes (19 top-level projects mention the canonical audit format; update tier counts 27->29 top-level / 9->7 _wip). - OVERVIEW.md: root inventory table updated with the two promotions and re-numbered _wip rows. - TODO.md: handoff updates. - Remaining _wip projects: assorted pass 2/3 edits (prg.s, locals.inc, reconstructed.lbl, reconstructed_prg.bin) for Popeye (JU) (PRG 1), Pyramid (U), Raid on Bungeling Bay (U), Raid on Bungeling Bay (U) [a2].
10 KiB
AGENTS.md
What this repo is
Thirty-six AI-generated disassemblies of NES ROMs: 27 promoted to top-level folders and 9 still under _wip/. All 36 carry real disassemblies (Pass 1 or beyond); the difference is the build tier — the 27 at top level verify byte-identical for the whole ROM, the 9 in _wip/ verify at the PRG level only. The contract (stated in PROMPTS.md and verified by make in every copilot/ folder) is byte-identical rebuilds: reconstructed.nes must match the original ROM exactly. Not for distribution (README.md).
Layout
- Each project owns a folder with a
copilot/build dir (prg.s,linker.cfg,symbols.inc,locals.inc,Makefile; promoted projects addheader.s,chr.sand a copy/symlinkrom.nes) and a per-projectOVERVIEW.mdat the folder root (power-on flow analysis). Some older ones usemain.sinstead ofprg.s, and a few lackprg.bin(carve the PRG out ofrom.nesinstead). - Root
OVERVIEW.mdis the index: vectors, build fixes, and summary for all 36 projects. Keep it in sync when a project changes — its DK Jr. entries drifted badly before being corrected, and its vector column later turned out to have five rows wrong from copy-paste duplication. TODO.mdis the handoff: open work per project, plus the gotchas that cost real time. Read it before starting anything non-trivial.PROMPTS.mdis the workflow contract.default_project.tar.gzis the empty-project skeleton (prg.s, symbols.inc, linker.cfg, Makefile, locals.inc)._ROMS/is the master ROM store._tools/:nesdis.py(Pass 1 tracer),pass2.pyandbytecode_dis.py(Pass 2/3 emitters),apply_symbols.py(substitutessymbols.inc/locals.incnames intoprg.soperands),trace_stage_payload.py(py65-based runtime tracer),cdl_reclass.py(Mesen-CDL-driven code/data split fix),verify_pass1.py(rebuild + re-trace check across the top-level projects),audit_comment_tool.py(adds canonical audit comments using tracer as ground truth),splitnes.py,chr2gif.py.
Commands
cd "<Project>/copilot" && make # builds reconstructed.nes, then cmp vs rom.nes
python3 _tools/verify_pass1.py # rebuilds + re-traces the 19 top-level projects
make's default target includes the byte-identity cmp — a build that doesn't print "Byte-identical ROM verified." is a failure. Requires ca65/ld65 on PATH.
There are two build tiers, and the difference is real. Top-level projects compare the whole ROM against copilot/rom.nes. The 17 _wip/ projects only run cmp reconstructed_prg.bin prg.bin, so their header and CHR are not proven — do not describe them as byte-identical to the ROM.
Promoting a project out of _wip/
git mv "_wip/<name>" "<name>"— the relativecopilot/rom.nes -> ../<name>.nessymlink survives untouched.- Add
copilot/rom.nes(a space-free copy or symlink of the original) and switch the Makefile fromcmp reconstructed_prg.bin prg.bintocmp reconstructed.nes rom.nes. 2b. A_wip/project has noheader.s/chr.sand itslinker.cfgdeclares only the PRG memory area, so add all three (copy the shape fromBurger Time (U)/copilot/: two one-line.incbins andHEADER: start = $0000, size = $0010ahead of PRG) or the whole-ROM compare has nothing to link.Lunar Pool (U)is the worked example (2026-10-18), andDonkey Kong (JU)(2026-10-19) the second — it also fixed a header-byte mirroring error in its projectOVERVIEW.md(Vertical → Horizontal from byte 6 =$00). - Change
../../../_tools/to../../_tools/in the Makefile — the depth changed by one. (No-op if the Makefile never referenced_tools/—Donkey Kong (JU)had none, and nomake regentarget either.) - Verify
make, thenmake regenif a regen pipeline exists. - Add it to the root inventory table and re-number the
_wiprows.
Irreversible conventions (do not "clean up" past these)
- Every instruction carries a trailing audit comment with its CPU address, PRG offset and exact emitted bytes. The canonical format is
; CPU:$C123 PRG:0123 AD 21 03— all 19 top-level projects now use this exact format (standardized 2026-10-19; Donkey Kong (JU) confirmed canonical at its promotion the same day). This is the byte-identity audit trail — preserve it even while renaming labels or restructuring files. The earlier gap (6 variants, 1,013 uncommented lines in Excitebike) was closed 2026-10-19 via_tools/audit_comment_tool.py. - Unreached/unresolved bytes stay as data (
Label_/Data/.byteor.incbinblocks with ROM provenance), never deleted or reformatted. - Routine headers follow the template in
prg.s: global + local description, parameters, returns, clobbers. - Two separate symbol files:
symbols.inc= NES hardware constants only;locals.inc= zero-page and forward references. Unresolved zero-page bytes keep neutralZpXXnames rather than speculative names. Proven semantic names may sit beside the generatedZpXXentries as aliases. - Pass discipline: Pass 1 = code/data split (stop for review), Pass 2 = label replacement, Pass 3 = semantic naming of data tables/structures. Do not skip straight to guessy rewrites. When a pass is only partly achievable, record the residue and why rather than inventing names.
make regen(where it exists, e.g. MUSCLE) must keep reproducingprg.sbyte-for-byte on the standard library alone. Re-check it after anynesdis.pychange.
Hard-won quirks (details in root OVERVIEW.md per-project "Build fix")
- The flat disassembler mislabels
bneasbpl; fixes usebne *-$XXto reproduce the original bytes (Clu Clu Land, Defender 2, Excitebike). - Zero pages are reused across modes; aliases may be required in
locals.inc(e.g.RandomStateAlt = $E4besideRandomState0 = $54). - Donkey Kong Jr. is architecturally different: reset entry is $C66E, the reset path idles forever in an LFSR loop, and the entire frame state machine runs inside NMI. Its
DeferredTaskHandlerTableentry 0 deliberately mirrors the IRQ vector word at $BFFE. - IRQ vectors often point at unused/data targets in these games — verify before assuming a handler is broken.
- Unlicensed titles break static-trace assumptions: Pyramid (U) pokes a mapper unlock at
$4100(beyond the register space) despite a mapper-0 header, and dispatches throughjmp ($0002)built from RAM — trace via its two queue tables ($C3A6,$CCD5), and do NOT fall-through-seed gaps that followrts/jmp/brk(that misclassifies data as code). - ROM filenames contain spaces and parentheses (e.g.
M.U.S.C.L.E. (U).nes); buildsincbina space-freerom.nesinsidecopilot/. Keep that copy/symlink working and don't rename it. Note that a name with spaces/parens is also what makes FCEUX hang headless — symlink it to a clean name and run under Xvfb. .gitignorecontainsROMS/*but the actual directory is_ROMS— the pattern matches nothing, so ROM binaries stay tracked. Check before committing large blobs.
Tracer and toolchain gotchas (2026-10-17)
Full detail and reasoning in TODO.md. These are the ones that silently corrupt results rather than failing loudly.
- Undocumented opcodes silently truncate a trace. A tracer that knows only the 151 official opcodes halts on the first one used as ordinary code. Joypad Test Cartridge died at
$C09EonDCP $3C20($CF) and filed 3,256 bytes of real code as.byte.nesdis.py --undocumenteddecodes all 105 (default off, so existingmake regenpipelines stay byte-stable). - ca65 V2.19 implements none of the undocumented opcodes, so they cannot be written as mnemonics.
nesdis.pyemits them as explicit.bytewith the disassembly in the comment — that keeps them in the code region, so both the split and the audit trail survive. ca65 is also happy to accept plainnop, which is official (0xEA). - Vectors are not in the iNES header. They are the three little-endian words at the end of the 16 KB PRG (
$FFFA–$FFFFin the$C000mirror). Header bytes 0–9 carry only sizes, mirroring and mapper. Parsing them fromheader.binas if they were header fields yields garbage. - RESET and IRQ are usually the same address. The semantic-name map must not overwrite on collision, or the primary entry gets renamed to
IrqHandler. Usesetdefaultin RESET/NMI/IRQ priority order. - Control-flow targets must win over fall-through. A fall-through decode whose operand spans an address that another path branches to is the wrong decode — no assembler branches mid-instruction. Mario Bros had a
bneat$C4BAtargeting$C528swallowed by aNOP #$A5at$C527. - Branch targets inside a surviving instruction must be pruned from the target set, or the emitter's linear walk consumes those bytes as an operand, the label is referenced but never defined, and the file does not assemble at all.
- A plain vector trace reaches only ~15% of a real game. Almost everything sits behind computed dispatch. Look for
jmp ($xx), and for dispatchers that pop the table address withpla— that form cannot be followed from the dispatcher because the pointer is only knowable in the caller's frame (Mario Bros$CD9E). Probing a suspected data region linearly withnesdis.py --probeis cheap and settles whether it is code;--auto-seedplus a couple of--extra-seedaddresses took Mario Bros from 14.6% to 72.2%. - When emulating to trace, NMI must fire once per frame, not per
$2002read. The main loop polls$2002to sync on vblank, and each read clears bit 7, re-arming NMI within the same scanline. Guarding on the vblank flag alone gave 40,900 NMIs across 3,000 frames and flattened coverage at 26%. - Controller input goes on joy1 =
$4016with a real latch/shift register. The poll routine'slda $4016,Xwith X=1 lands on$4017, but only bits 0–1 are compared, so driving$4017consumes nothing and changes no coverage at all. ca65 -llistings contain no source line numbers — they emit<offset>r <n> <bytes> <source text>. You cannot key listing entries back to source lines, and the listing printsrr rrfor relocatable words, so byte values must come fromprg.bin.- py65 is not a build dependency. Unpack on demand (
pip download --no-deps py65, thenunzipinto a lib dir;PY65LIBenv var points at it)./tmpis volatile — a reboot wiped it mid-session. Anything reusable belongs in_tools/.