{"owner":"felixrieseberg","repo":"windows95","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/inspect-disk/SKILL.md",".claude/skills/update-v86/SKILL.md",".claude/skills/probe-win95/SKILL.md"],"skills":{".claude/skills/inspect-disk/SKILL.md":"---\nname: inspect-disk\ndescription: Inspect or modify the Windows 95 disk image offline with mtools — no QEMU, no Electron, no boot. Use when checking what's on the C: drive, extracting/reading files from the image, copying files into it, or verifying image contents after a change.\n---\n\n# Inspecting windows95.img without booting it\n\n`images/windows95.img` is a 1 GB raw MBR disk with a single FAT32 (0x0C)\npartition starting at sector 63. mtools (installed via Homebrew) can read\nand write it directly — the only trick is the partition byte offset:\n\n```\n63 sectors × 512 bytes = 32256\n```\n\nEvery command takes the same image spec: `-i images/windows95.img@@32256 ::`\n\n## Read operations (always safe)\n\n| Task | Command |\n|---|---|\n| List root | `mdir -i images/windows95.img@@32256 ::` |\n| List a subdir | `mdir -i images/windows95.img@@32256 ::/WINDOWS` |\n| Recursive bare listing | `mdir -/ -b -i images/windows95.img@@32256 ::` |\n| Print a text file | `mtype -i images/windows95.img@@32256 ::/AUTOEXEC.BAT` |\n| Extract a file to host | `mcopy -i images/windows95.img@@32256 ::/CONFIG.SYS /tmp/` |\n| Extract a dir recursively | `mcopy -s -i images/windows95.img@@32256 ::/TOOLS /tmp/tools` |\n| Filesystem info | `minfo -i images/windows95.img@@32256 ::` |\n| Disk usage (in 4 KB clusters) | `mdu -i images/windows95.img@@32256 ::` |\n\nLong filenames work (`PROGRA~1` ↔ `Program Files`); quote paths with\nspaces. Paths use `::/` as the root of C:.\n\n**Hidden files need `-a`.** Plain `mdir` silently skips hidden/system files\n(`BOOTLOG.TXT`, `SYSTEM.DA0`, `ShellIconCache`, `ie5bak.DAT`, …). Existence\nchecks must use `mdir -a`, or you'll wrongly conclude a file isn't there.\n\n**Check the image isn't in use first.** A running Electron *or QEMU* session\nholds the image open and writes to it lazily (Win95's VCACHE flushes on its\nown schedule):\n\n```sh\nlsof images/windows95.img   # must come back empty\n```\n\nA copy taken while the VM runs will have FAT inconsistencies (directory\nentries written, FAT not yet flushed). To repair a copy:\n\n```sh\nDEV=$(hdiutil attach -imagekey diskimage-class=CRawDiskImage -nomount copy.img | head -1 | awk '{print $1}')\nfsck_msdos -y \"${DEV}s1\"\nhdiutil detach \"$DEV\"\n```\n\n## Write operations (read the warnings first)\n\n| Task | Command |\n|---|---|\n| Copy file into image | `mcopy -o -i images/windows95.img@@32256 /host/file.txt ::/TOOLS/` |\n| Make a directory | `mmd -i images/windows95.img@@32256 ::/NEWDIR` |\n| Delete a file | `mdel -i images/windows95.img@@32256 ::/FILE.TXT` |\n| Delete a dir recursively | `mdeltree -i images/windows95.img@@32256 ::/DIR` |\n| Clear R/S/H attributes | `mattrib -i images/windows95.img@@32256 -r -s -h ::/FILE` (`-/` for recursive) |\n\n`mdel`/`mdeltree` fail on read-only/hidden/system files — run `mattrib`\nfirst. Note that bulk offline cleanup is currently discouraged: offline\nmodification worsens v86 cold-boot reliability. Prefer doing cleanup\ninside Windows; see \"Slimming the image\" in `docs/qemu.md`.\n\n### Warning 1: never write while a VM holds the image\n\nIf Electron/v86 or QEMU is running against the image, offline writes race\nthe guest's own writes and corrupt the FAT. Check first:\n\n```sh\npgrep -fl \"windows95.*electron|qemu.*windows95\"\n```\n\n### Warning 2: any offline write must be verified with the in-app probe\n\nQEMU and v86 exercise different boot paths (different hardware → different\ndriver init), and offline modification can break v86 cold boot while QEMU\nstill boots fine (\"Invalid VxD dynamic link call\" — see \"VXDLINK: flake\nvs. real bug\" in the probe-win95 skill). Verify with `tools/probe-boot.sh`,\nnot just `yarn run qemu`. In particular, **never zero free space via the\nmcopy-a-giant-zero-file trick** — that breaks v86 cold boot\ndeterministically; use `tools/zero-free-clusters.py` if you must zero free\nspace at all.\n\n### Warning 3: saved states cache the old disk\n\n`images/default-state.bin` and `~/Library/Application Support/windows95/state-v*.bin`\ncontain Win95's RAM, including VCACHE/FAT caches that reference the disk\n*as it was when the state was saved*. Modifying the disk offline and then\nresuming a saved state = filesystem corruption.\n\nAfter any offline write:\n\n```sh\nrm -f \"$HOME/Library/Application Support/windows95/\"state-v*.bin\n```\n\n…so the next boot is fresh (or from `default-state.bin`, which boots from\ndisk early enough to be safe only if the files you touched weren't open at\nstate-save time — when in doubt, do a full fresh boot and re-save).\n\n## Alternative: mount in Finder\n\n```sh\nhdiutil attach -imagekey diskimage-class=CRawDiskImage -readonly images/windows95.img\n# … browse /Volumes/WIN95 …\nhdiutil detach /Volumes/WIN95\n```\n\nRead-only is deliberate; use mtools for writes so you control exactly what\nchanges.\n\n## Worktree note\n\n`images/` is gitignored — in a fresh worktree there is no image to inspect.\nSee \"Running from a git worktree\" in the probe-win95 skill for the\nclone-from-main-checkout snippet.\n\n## Why not QEMU/v86?\n\nBooting to inspect the disk takes ~40s+ per round trip (see `probe-win95`)\nand every boot mutates the image (registry, swap, SCANDISK.LOG). mtools is\ninstant and, for read operations, leaves the image bit-identical — which\nmatters when you're trying to diff or bisect image changes.\n",".claude/skills/update-v86/SKILL.md":"---\nname: update-v86\ndescription: Build and install v86 (wasm + libv86.js + BIOS) into windows95. Use when pulling upstream v86 changes, fixing a broken build, verifying the fork branches are still in sync, or setting up a fresh v86 checkout.\n---\n\n# Updating v86\n\nwindows95 builds v86 from source — not from copy.sh. Two small bugfix\npatches ride along on a fork branch until the upstream PRs land.\n\n## Sources\n\n| File | Built from |\n|---|---|\n| `src/renderer/lib/libv86.js` | `make build/libv86.js` in `../v86` |\n| `src/renderer/lib/build/v86.wasm` | `make build/v86.wasm` |\n| `bios/seabios.bin`, `bios/vgabios.bin` | copied from `../v86/bios/` |\n\n`tools/update-v86.js` runs those targets, copies the artifacts, runs 5\nsanity checks, and fails loudly if any prerequisite is missing. No\nfallbacks, no fetching from copy.sh.\n\n## The fork branch\n\nv86 should be checked out on **`felixrieseberg/v86:windows95-base`**.\nThat branch merges the feature branches tracked in\n[`docs/v86-patches.md`](../../../docs/v86-patches.md) — keep that table\nin sync with this list. Each is upstreamable on its own:\n\n- **`electron-renderer-fs-loader`** (PR #1540) — `src/lib.js` uses\n  `require(\"fs\")` instead of `await import(\"node:fs/promises\")`. Dynamic\n  import of `node:` URLs doesn't work in an Electron renderer.\n- **`ide-shared-registers`** (PR #1541) — `src/ide.js` writes ATA Command\n  Block registers (Features, Sector Count, LBA Low/Mid/High) to both\n  master and slave. Without this, Win95/98 hang at the splash screen on\n  any disk >~535MiB. Root cause: v86 commit `1b90d2e7` changed those\n  writes to target only `current_interface`, but per ATA spec they're\n  channel-shared (one register file on the IDE cable; both drives latch\n  the same value).\n- **`vmware-abspointer`** — `src/vmware.js` implements the VMware\n  backdoor (port `0x5658`): GETVERSION + ABSPOINTER_* so a guest driver\n  (VBADOS VBMOUSE) can read absolute cursor position and track the host\n  cursor 1:1 without pointer lock, and the legacy text-clipboard commands\n  6–9 so `W95TOOLS.EXE` (guest-tools/agent) can sync `CF_TEXT` with\n  the host. Consumes `mouse-absolute` and `vmware-clipboard-host` bus\n  events; emits `vmware-absolute-mouse` and `vmware-clipboard-guest`.\n- **`vmware-gettime`** *(stacked on the clipboard branch)* —\n  `src/vmware.js` adds backdoor command GETTIME (23): EAX = host UTC\n  seconds, EBX = microseconds, ECX = max time lag, EDX = host UTC offset\n  in minutes. `W95TOOLS.EXE` polls it and calls `SetLocalTime`, so a\n  resumed guest (which never re-reads the RTC) snaps back to host time.\n- **`fake-network-copy-tcp-addrs`** — `src/browser/fake_network.js`\n  copies the four address subarrays (`hsrc/hdest/psrc/pdest`) when a\n  `TCPConnection` is created from an inbound SYN. Upstream stores them\n  as zero-copy views into the NE2000 TX ring; once the guest's 12-slot\n  TX ring wraps (any concurrent traffic — SMB, NBNS, ping), `pump()`\n  builds segments with whatever IP now occupies that slot, the guest\n  RSTs them as belonging to no TCB, and `recv()` blocks forever.\n  Exercised by `tools/probe-tcp.sh`.\n- **`vga-defer-vbe-disable-v86`** — `src/vga.js` defers `dispi[4]=0`\n  written from V86 mode until a legacy attribute-mode write reaches the\n  hardware. Win9x's VDD virtualises ports 3B0–3DF for a windowed DOS VM\n  but not 1CE/1CF, so vgabios's VBE-disable leaks through while the rest\n  of its mode-set is captured into the VM's virtual register file —\n  without this the screen turns to planar garbage the moment you open a\n  DOS box.\n\n## Prerequisites\n\n```sh\nrustup target add wasm32-unknown-unknown\nbrew install openjdk\n# one-time: fetch the Closure compiler v86's Makefile pins to\ncurl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar \\\n  -o ../v86/closure-compiler/compiler.jar\n```\n\nClosure **must** be v20210601 — newer versions hit\n[closure-compiler#3972](https://github.com/google/closure-compiler/issues/3972)\non v86's source. The pin is in v86's Makefile.\n\n## Steps\n\n```sh\ncd ../v86\ngit fetch fork origin\ngit checkout windows95-base\ngit rebase fork/windows95-base   # in case fork was updated elsewhere\ncd ../windows95\nnode tools/update-v86.js\n```\n\nThat's it. Script runs both `make` targets, copies, verifies.\n\n## Sanity-check WARNs\n\nThe 5 checks assert invariants `src/renderer/smb/index.ts` and\n`tools/parcel-build.js` depend on. A WARN means upstream changed\nsomething load-bearing — don't ignore it:\n\n1. **`await import(\"node:...\")` still present** → PR #1540 was reverted\n   or the pattern moved. Electron renderer will fail to load disk images.\n2. **`master.features_reg=` missing in minified** → PR #1541 was reverted\n   or `windows95-base` lost the commit. Win95 will hang at splash on\n   disks >535MiB. Check `cd ../v86 && git log --oneline windows95-base`.\n3. **Export pattern changed** → `tools/parcel-build.js` shim needs\n   updating. Look for `module.exports.V86=` and `window.V86=`.\n4. **`tcp-connection` event gone** → SMB falls back to the old-API theft\n   hack in `src/renderer/smb/index.ts` — still works, but surprising.\n5. **`on_tcp_connection` gone** → old-API fallback is dead. SMB integration\n   only works via the `tcp-connection` bus event now. Harmless; update\n   the comment in `index.ts` and retire the theft code.\n\n## After updating, probe-test\n\n```sh\nnode tools/update-v86.js && tools/probe-boot.sh\n```\n\nShould land SUCCESS in ~40s. If FAIL_SPLASH_HANG, the IDE fix didn't\ntake — check `grep master.features_reg src/renderer/lib/libv86.js`. If\nFAIL_VXDLINK, retry — sporadic bluescreens are normal (see the\n`probe-win95` skill).\n\n## When a PR merges upstream\n\nRebase `windows95-base` to drop the now-redundant commit:\n\n```sh\ncd ../v86\ngit fetch origin\ngit checkout windows95-base\ngit rebase origin/master              # drops the merged commit cleanly\ngit push fork windows95-base --force-with-lease\n```\n\nIf **both** PRs are upstream, retire the fork branch entirely:\n\n1. Point `tools/update-v86.js` default at `origin/master` (it already\n   uses `../v86`, so just `git checkout master` there)\n2. Delete `fork/windows95-base`\n3. Remove this skill's \"The fork branch\" section\n4. Confirm the 5 sanity checks still pass — they're version-agnostic\n\n## Integration contract with SMB\n\nThe SMB server sits on top of v86's network adapter. Details in\n`src/renderer/smb/README.md`. Short version: the new path uses the\n`tcp-connection` bus event; the fallback path uses\n`adapter.on_tcp_connection` callback + connection-theft (stealing a\n`TCPConnection` the HTTP probe builds for us). Both use `.on_data` on\nthe conn, not `.on(\"data\")`, because Closure dead-code-eliminates the\nevent emitter plumbing.\n\nIf any v86 update breaks these assumptions, `src/renderer/smb/index.ts`\nneeds updating, not just `tools/update-v86.js`.\n",".claude/skills/probe-win95/SKILL.md":"---\nname: probe-win95\ndescription: Boot Windows 95 in Electron under Claude's control, without a human clicking anything. Use when testing v86 updates, SMB changes, keyboard input, boot stability, or bisecting regressions.\n---\n\n# Probing Windows 95 autonomously\n\nYou can run and test the Win95 VM yourself. The harness is already wired\nup — three pieces:\n\n| File | Role |\n|---|---|\n| `src/renderer/debug-harness.ts` | Activated by `WIN95_PROBE=1`. Boots fresh automatically, samples CPU + VGA + text screen every 5s, writes `/tmp/win95-probe.json` + `/tmp/win95-screen.png`, detects SUCCESS vs FAIL modes, optionally drives keyboard input. |\n| `src/renderer/smb/index.ts` | Wraps `console.log` so `[smb]` and `[nbns]` lines tee to `$TMPDIR/windows95-smb.log` (outside Electron, readable by any polling script — no CDP needed). |\n| `tools/probe-boot.sh` | One-shot: kill leftovers → parcel build → launch Electron → poll `/tmp/win95-probe.done` → report → kill. |\n\n## Running from a git worktree\n\n`images/` is gitignored, so a fresh worktree has no disk image or default\nstate and every probe will fail at boot. Clone them from the main checkout\nfirst (APFS clonefile — instant, no extra disk space):\n\n```sh\nmkdir -p images\ncp -c \"$(git rev-parse --git-common-dir)/..\"/images/*.{img,bin} images/\n```\n\n## One-shot boot test\n\n```sh\ntools/probe-boot.sh\n```\n\nPrints SUCCESS or a FAIL verdict. ~40s on a clean run.\n\n## Boot + type into Run\n\n```sh\npkill -9 -f \"windows95.*electron\"; sleep 2\nrm -f \"$HOME/Library/Application Support/windows95/\"state-v*.bin\nrm -f /tmp/win95-probe.json /tmp/win95-probe.done \\\n      \"$TMPDIR/windows95-smb.log\"\n\nWIN95_PROBE=1 \\\nWIN95_PROBE_SCRIPT='HOST/HOST' \\\nWIN95_SMB_SHARE=\"$HOME/Downloads\" \\\n  ./node_modules/.bin/electron . > /tmp/win95-electron.log 2>&1 &\n```\n\n`WIN95_PROBE_SCRIPT='HOST/HOST'` types `\\\\HOST\\HOST` into Start → Run on\ndesktop. `WIN95_PROBE_DOSBOX=1` instead opens `command`, types `dir`,\nand (with `WIN95_PROBE_DOSBOX_ALTENTER=1`) toggles fullscreen — this is\nthe regression scenario for the windowed-DOS-box VBE leak.\n`WIN95_PROBE_CDROM=/path/to.iso` mounts an ISO on the secondary-IDE\nATAPI drive (bypasses the settings UI). `WIN95_PROBE_CDTRACE=1` logs\nevery secondary-channel ATA/ATAPI command to `/tmp/win95-cdtrace.log`.\n`WIN95_PROBE_VGATRACE=1` wraps the VGA I/O ports at the `io.ports[]`\nlayer and writes `[port, op, value, \"eip VMPE cplN\"]` tuples to\n`/tmp/win95-vgatrace.json` every tick (heavy — can hit 1M entries during\nboot). `/` → `\\` substitution (env var / shell quoting, pragmatism). The\nharness drives it via XT scancodes — Win95 doesn't have Win+R (Win98+\nonly), so the sequence is Esc, Esc, Ctrl+Esc, R, backslashes + text,\nEnter.\n\n## Reading results\n\n| File | What |\n|---|---|\n| `/tmp/win95-probe.json` | Live status: `phase` (`init`/`text-mode`/`splash`/`desktop`), `gfxW/H`, `textScreen`, `instructionDelta`, `verdict` |\n| `/tmp/win95-probe.done` | Written once when verdict is decided |\n| `/tmp/win95-screen.png` | Canvas screenshot, refreshed each tick |\n| `$TMPDIR/windows95-smb.log` | SMB/NBNS protocol trace |\n| `/tmp/win95-electron.log` | Electron stderr |\n\n## Verdicts\n\n| Verdict | Meaning | Action |\n|---|---|---|\n| `SUCCESS` | Canvas ≥640×480, CPU active, uptime >30s | desktop reached |\n| `FAIL_VXDLINK` | \"Invalid VxD dynamic link call\" | flaky — retry |\n| `FAIL_IOS` / `FAIL_PROTECTION` | IOS subsystem protection error | usually driver/BIOS mismatch |\n| `FAIL_KRNL386` | \"Cannot find KRNL386.EXE\" in safe mode | disk reads returning garbage — wasm/BIOS drift |\n| `FAIL_SPLASH_HANG` | Canvas stuck 320×400 for >70s | IRQ starvation — if you're on v86 master, check the IDE register fix |\n| `FAIL_HUNG` | CPU stopped advancing or text screen frozen 40s | hard hang |\n\n## Rules of the road\n\n- **Sporadic bluescreens are normal** on all v86 versions. One FAIL_VXDLINK\n  or FAIL_HUNG doesn't prove anything — retry up to 3×.\n- **Always clean state** (`state-v*.bin` — the suffix tracks `STATE_VERSION`\n  in `src/constants.ts`, so never hardcode a version) before a probe. `pkill`\n  on a wedged Electron triggers `onbeforeunload`, saving the *corrupted*\n  state. Deleting it forces fallback to `images/default-state.bin`.\n- **Don't trust the text buffer in graphics mode.** After desktop (≥640×480)\n  the stale BIOS text lingers in the buffer. The harness's `phase` field\n  accounts for this; don't re-read `textScreen` in a `desktop` phase and\n  think you hit a BSOD.\n- **Kill Electron when done.** Background processes pile up, each holding\n  the disk image lock. `pkill -f \"windows95.*electron\"` on every path out.\n\n## Bisecting v86\n\n`tools/bisect-v86.sh <commit>` handles one step. The harness retries 3×\nper commit. Hard-won lessons:\n\n1. **Validate bounds against a known-good binary.** Source-built wasm can\n   drift from prod due to cargo/rustc version differences. We hit this:\n   the \"GOOD\" bound produced a wasm that couldn't read the disk at all.\n2. **JS-only when toolchain drifts.** Keep the prod wasm, rebuild only\n   libv86.js at each commit. Closure is deterministic enough; cargo\n   isn't always. Works until you cross a commit that changes the JS↔wasm\n   ABI (for v86, the APIC→Rust port in Aug 2025).\n3. **Retry on FAIL, never on SUCCESS.** One SUCCESS = commit is good.\n   Three different FAILs at the same commit = commit is bad.\n4. **State cleanup between runs** (see above). Skipping this is the #1\n   cause of spurious \"bad\" verdicts during bisect.\n\n## Extending the harness\n\n- New verdicts: add to the chain in `collectStatus` in `debug-harness.ts`\n- New keyboard actions: extend `runScript` (current types: `keys`, `chord`,\n  `text`, `wait`)\n- New probe signals: add to `ProbeStatus` interface\n\nGate everything new on `process.env.WIN95_PROBE === \"1\"` so it stays out\nof the normal app.\n\n## Common failure diagnostics\n\n| Symptom | Check |\n|---|---|\n| No SMB traffic at all | `$TMPDIR/windows95-smb.log` should have `hooked adapter` line. If absent, v86 API changed — see `src/renderer/smb/README.md` |\n| SMB hooks fire, no connection | Win95's \"NetBIOS over TCP/IP\" checkbox — bake into default-state.bin |\n| Boot hangs on `2996c087` or older v86 | You probably have a ABI-mismatched wasm/JS pair. Prod wasm is the ground truth; rebuild JS against it. |\n\n## VXDLINK: flake vs. real bug\n\nTwo different things produce FAIL_VXDLINK:\n\n1. **Sporadic flake** (~1 in 2–3 runs even on known-good images): passes on\n   retry. This is why the retry-3× rule exists.\n2. **Deterministic failure** (same address every run, e.g.\n   `VMM(01)+000036E5 → device \"C000\" service E3E4`): the image's disk\n   layout triggers a real v86 disk-path bug. Known triggers: zeroing free\n   space via the \"mcopy a giant zero file, then delete it\" trick, and\n   offline (mtools) mass-deletion of recently-written file trees. The same\n   image boots fine in QEMU and passes fsck. Retrying never helps; the\n   image content must change.\n\nThis is the canonical verdict-interpretation policy (other docs link here):\n\n- **One SUCCESS** = the image can boot. Good — same rule as bisecting.\n- **Three identical failures** (same VxD address) = the image is in a bad\n  state. Stop retrying; the content must change.\n- Anything in between = keep retrying, you are looking at flakes.\n\nNever conclude anything from a single FAIL.\n\n## Probing the state-restore path\n\n`WIN95_PROBE_RESTORE=1 tools/probe-boot.sh` makes the probe restore state\n(user state → `images/default-state.bin` fallback) instead of cold\nbooting. Use it to verify a freshly generated default-state.bin actually\nresumes to the desktop — required after every image change.\n"},"files":{".claude/skills/inspect-disk/SKILL.md":"---\nname: inspect-disk\ndescription: Inspect or modify the Windows 95 disk image offline with mtools — no QEMU, no Electron, no boot. Use when checking what's on the C: drive, extracting/reading files from the image, copying files into it, or verifying image contents after a change.\n---\n\n# Inspecting windows95.img without booting it\n\n`images/windows95.img` is a 1 GB raw MBR disk with a single FAT32 (0x0C)\npartition starting at sector 63. mtools (installed via Homebrew) can read\nand write it directly — the only trick is the partition byte offset:\n\n```\n63 sectors × 512 bytes = 32256\n```\n\nEvery command takes the same image spec: `-i images/windows95.img@@32256 ::`\n\n## Read operations (always safe)\n\n| Task | Command |\n|---|---|\n| List root | `mdir -i images/windows95.img@@32256 ::` |\n| List a subdir | `mdir -i images/windows95.img@@32256 ::/WINDOWS` |\n| Recursive bare listing | `mdir -/ -b -i images/windows95.img@@32256 ::` |\n| Print a text file | `mtype -i images/windows95.img@@32256 ::/AUTOEXEC.BAT` |\n| Extract a file to host | `mcopy -i images/windows95.img@@32256 ::/CONFIG.SYS /tmp/` |\n| Extract a dir recursively | `mcopy -s -i images/windows95.img@@32256 ::/TOOLS /tmp/tools` |\n| Filesystem info | `minfo -i images/windows95.img@@32256 ::` |\n| Disk usage (in 4 KB clusters) | `mdu -i images/windows95.img@@32256 ::` |\n\nLong filenames work (`PROGRA~1` ↔ `Program Files`); quote paths with\nspaces. Paths use `::/` as the root of C:.\n\n**Hidden files need `-a`.** Plain `mdir` silently skips hidden/system files\n(`BOOTLOG.TXT`, `SYSTEM.DA0`, `ShellIconCache`, `ie5bak.DAT`, …). Existence\nchecks must use `mdir -a`, or you'll wrongly conclude a file isn't there.\n\n**Check the image isn't in use first.** A running Electron *or QEMU* session\nholds the image open and writes to it lazily (Win95's VCACHE flushes on its\nown schedule):\n\n```sh\nlsof images/windows95.img   # must come back empty\n```\n\nA copy taken while the VM runs will have FAT inconsistencies (directory\nentries written, FAT not yet flushed). To repair a copy:\n\n```sh\nDEV=$(hdiutil attach -imagekey diskimage-class=CRawDiskImage -nomount copy.img | head -1 | awk '{print $1}')\nfsck_msdos -y \"${DEV}s1\"\nhdiutil detach \"$DEV\"\n```\n\n## Write operations (read the warnings first)\n\n| Task | Command |\n|---|---|\n| Copy file into image | `mcopy -o -i images/windows95.img@@32256 /host/file.txt ::/TOOLS/` |\n| Make a directory | `mmd -i images/windows95.img@@32256 ::/NEWDIR` |\n| Delete a file | `mdel -i images/windows95.img@@32256 ::/FILE.TXT` |\n| Delete a dir recursively | `mdeltree -i images/windows95.img@@32256 ::/DIR` |\n| Clear R/S/H attributes | `mattrib -i images/windows95.img@@32256 -r -s -h ::/FILE` (`-/` for recursive) |\n\n`mdel`/`mdeltree` fail on read-only/hidden/system files — run `mattrib`\nfirst. Note that bulk offline cleanup is currently discouraged: offline\nmodification worsens v86 cold-boot reliability. Prefer doing cleanup\ninside Windows; see \"Slimming the image\" in `docs/qemu.md`.\n\n### Warning 1: never write while a VM holds the image\n\nIf Electron/v86 or QEMU is running against the image, offline writes race\nthe guest's own writes and corrupt the FAT. Check first:\n\n```sh\npgrep -fl \"windows95.*electron|qemu.*windows95\"\n```\n\n### Warning 2: any offline write must be verified with the in-app probe\n\nQEMU and v86 exercise different boot paths (different hardware → different\ndriver init), and offline modification can break v86 cold boot while QEMU\nstill boots fine (\"Invalid VxD dynamic link call\" — see \"VXDLINK: flake\nvs. real bug\" in the probe-win95 skill). Verify with `tools/probe-boot.sh`,\nnot just `yarn run qemu`. In particular, **never zero free space via the\nmcopy-a-giant-zero-file trick** — that breaks v86 cold boot\ndeterministically; use `tools/zero-free-clusters.py` if you must zero free\nspace at all.\n\n### Warning 3: saved states cache the old disk\n\n`images/default-state.bin` and `~/Library/Application Support/windows95/state-v*.bin`\ncontain Win95's RAM, including VCACHE/FAT caches that reference the disk\n*as it was when the state was saved*. Modifying the disk offline and then\nresuming a saved state = filesystem corruption.\n\nAfter any offline write:\n\n```sh\nrm -f \"$HOME/Library/Application Support/windows95/\"state-v*.bin\n```\n\n…so the next boot is fresh (or from `default-state.bin`, which boots from\ndisk early enough to be safe only if the files you touched weren't open at\nstate-save time — when in doubt, do a full fresh boot and re-save).\n\n## Alternative: mount in Finder\n\n```sh\nhdiutil attach -imagekey diskimage-class=CRawDiskImage -readonly images/windows95.img\n# … browse /Volumes/WIN95 …\nhdiutil detach /Volumes/WIN95\n```\n\nRead-only is deliberate; use mtools for writes so you control exactly what\nchanges.\n\n## Worktree note\n\n`images/` is gitignored — in a fresh worktree there is no image to inspect.\nSee \"Running from a git worktree\" in the probe-win95 skill for the\nclone-from-main-checkout snippet.\n\n## Why not QEMU/v86?\n\nBooting to inspect the disk takes ~40s+ per round trip (see `probe-win95`)\nand every boot mutates the image (registry, swap, SCANDISK.LOG). mtools is\ninstant and, for read operations, leaves the image bit-identical — which\nmatters when you're trying to diff or bisect image changes.\n",".claude/skills/update-v86/SKILL.md":"---\nname: update-v86\ndescription: Build and install v86 (wasm + libv86.js + BIOS) into windows95. Use when pulling upstream v86 changes, fixing a broken build, verifying the fork branches are still in sync, or setting up a fresh v86 checkout.\n---\n\n# Updating v86\n\nwindows95 builds v86 from source — not from copy.sh. Two small bugfix\npatches ride along on a fork branch until the upstream PRs land.\n\n## Sources\n\n| File | Built from |\n|---|---|\n| `src/renderer/lib/libv86.js` | `make build/libv86.js` in `../v86` |\n| `src/renderer/lib/build/v86.wasm` | `make build/v86.wasm` |\n| `bios/seabios.bin`, `bios/vgabios.bin` | copied from `../v86/bios/` |\n\n`tools/update-v86.js` runs those targets, copies the artifacts, runs 5\nsanity checks, and fails loudly if any prerequisite is missing. No\nfallbacks, no fetching from copy.sh.\n\n## The fork branch\n\nv86 should be checked out on **`felixrieseberg/v86:windows95-base`**.\nThat branch merges the feature branches tracked in\n[`docs/v86-patches.md`](../../../docs/v86-patches.md) — keep that table\nin sync with this list. Each is upstreamable on its own:\n\n- **`electron-renderer-fs-loader`** (PR #1540) — `src/lib.js` uses\n  `require(\"fs\")` instead of `await import(\"node:fs/promises\")`. Dynamic\n  import of `node:` URLs doesn't work in an Electron renderer.\n- **`ide-shared-registers`** (PR #1541) — `src/ide.js` writes ATA Command\n  Block registers (Features, Sector Count, LBA Low/Mid/High) to both\n  master and slave. Without this, Win95/98 hang at the splash screen on\n  any disk >~535MiB. Root cause: v86 commit `1b90d2e7` changed those\n  writes to target only `current_interface`, but per ATA spec they're\n  channel-shared (one register file on the IDE cable; both drives latch\n  the same value).\n- **`vmware-abspointer`** — `src/vmware.js` implements the VMware\n  backdoor (port `0x5658`): GETVERSION + ABSPOINTER_* so a guest driver\n  (VBADOS VBMOUSE) can read absolute cursor position and track the host\n  cursor 1:1 without pointer lock, and the legacy text-clipboard commands\n  6–9 so `W95TOOLS.EXE` (guest-tools/agent) can sync `CF_TEXT` with\n  the host. Consumes `mouse-absolute` and `vmware-clipboard-host` bus\n  events; emits `vmware-absolute-mouse` and `vmware-clipboard-guest`.\n- **`vmware-gettime`** *(stacked on the clipboard branch)* —\n  `src/vmware.js` adds backdoor command GETTIME (23): EAX = host UTC\n  seconds, EBX = microseconds, ECX = max time lag, EDX = host UTC offset\n  in minutes. `W95TOOLS.EXE` polls it and calls `SetLocalTime`, so a\n  resumed guest (which never re-reads the RTC) snaps back to host time.\n- **`fake-network-copy-tcp-addrs`** — `src/browser/fake_network.js`\n  copies the four address subarrays (`hsrc/hdest/psrc/pdest`) when a\n  `TCPConnection` is created from an inbound SYN. Upstream stores them\n  as zero-copy views into the NE2000 TX ring; once the guest's 12-slot\n  TX ring wraps (any concurrent traffic — SMB, NBNS, ping), `pump()`\n  builds segments with whatever IP now occupies that slot, the guest\n  RSTs them as belonging to no TCB, and `recv()` blocks forever.\n  Exercised by `tools/probe-tcp.sh`.\n- **`vga-defer-vbe-disable-v86`** — `src/vga.js` defers `dispi[4]=0`\n  written from V86 mode until a legacy attribute-mode write reaches the\n  hardware. Win9x's VDD virtualises ports 3B0–3DF for a windowed DOS VM\n  but not 1CE/1CF, so vgabios's VBE-disable leaks through while the rest\n  of its mode-set is captured into the VM's virtual register file —\n  without this the screen turns to planar garbage the moment you open a\n  DOS box.\n\n## Prerequisites\n\n```sh\nrustup target add wasm32-unknown-unknown\nbrew install openjdk\n# one-time: fetch the Closure compiler v86's Makefile pins to\ncurl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar \\\n  -o ../v86/closure-compiler/compiler.jar\n```\n\nClosure **must** be v20210601 — newer versions hit\n[closure-compiler#3972](https://github.com/google/closure-compiler/issues/3972)\non v86's source. The pin is in v86's Makefile.\n\n## Steps\n\n```sh\ncd ../v86\ngit fetch fork origin\ngit checkout windows95-base\ngit rebase fork/windows95-base   # in case fork was updated elsewhere\ncd ../windows95\nnode tools/update-v86.js\n```\n\nThat's it. Script runs both `make` targets, copies, verifies.\n\n## Sanity-check WARNs\n\nThe 5 checks assert invariants `src/renderer/smb/index.ts` and\n`tools/parcel-build.js` depend on. A WARN means upstream changed\nsomething load-bearing — don't ignore it:\n\n1. **`await import(\"node:...\")` still present** → PR #1540 was reverted\n   or the pattern moved. Electron renderer will fail to load disk images.\n2. **`master.features_reg=` missing in minified** → PR #1541 was reverted\n   or `windows95-base` lost the commit. Win95 will hang at splash on\n   disks >535MiB. Check `cd ../v86 && git log --oneline windows95-base`.\n3. **Export pattern changed** → `tools/parcel-build.js` shim needs\n   updating. Look for `module.exports.V86=` and `window.V86=`.\n4. **`tcp-connection` event gone** → SMB falls back to the old-API theft\n   hack in `src/renderer/smb/index.ts` — still works, but surprising.\n5. **`on_tcp_connection` gone** → old-API fallback is dead. SMB integration\n   only works via the `tcp-connection` bus event now. Harmless; update\n   the comment in `index.ts` and retire the theft code.\n\n## After updating, probe-test\n\n```sh\nnode tools/update-v86.js && tools/probe-boot.sh\n```\n\nShould land SUCCESS in ~40s. If FAIL_SPLASH_HANG, the IDE fix didn't\ntake — check `grep master.features_reg src/renderer/lib/libv86.js`. If\nFAIL_VXDLINK, retry — sporadic bluescreens are normal (see the\n`probe-win95` skill).\n\n## When a PR merges upstream\n\nRebase `windows95-base` to drop the now-redundant commit:\n\n```sh\ncd ../v86\ngit fetch origin\ngit checkout windows95-base\ngit rebase origin/master              # drops the merged commit cleanly\ngit push fork windows95-base --force-with-lease\n```\n\nIf **both** PRs are upstream, retire the fork branch entirely:\n\n1. Point `tools/update-v86.js` default at `origin/master` (it already\n   uses `../v86`, so just `git checkout master` there)\n2. Delete `fork/windows95-base`\n3. Remove this skill's \"The fork branch\" section\n4. Confirm the 5 sanity checks still pass — they're version-agnostic\n\n## Integration contract with SMB\n\nThe SMB server sits on top of v86's network adapter. Details in\n`src/renderer/smb/README.md`. Short version: the new path uses the\n`tcp-connection` bus event; the fallback path uses\n`adapter.on_tcp_connection` callback + connection-theft (stealing a\n`TCPConnection` the HTTP probe builds for us). Both use `.on_data` on\nthe conn, not `.on(\"data\")`, because Closure dead-code-eliminates the\nevent emitter plumbing.\n\nIf any v86 update breaks these assumptions, `src/renderer/smb/index.ts`\nneeds updating, not just `tools/update-v86.js`.\n",".claude/skills/probe-win95/SKILL.md":"---\nname: probe-win95\ndescription: Boot Windows 95 in Electron under Claude's control, without a human clicking anything. Use when testing v86 updates, SMB changes, keyboard input, boot stability, or bisecting regressions.\n---\n\n# Probing Windows 95 autonomously\n\nYou can run and test the Win95 VM yourself. The harness is already wired\nup — three pieces:\n\n| File | Role |\n|---|---|\n| `src/renderer/debug-harness.ts` | Activated by `WIN95_PROBE=1`. Boots fresh automatically, samples CPU + VGA + text screen every 5s, writes `/tmp/win95-probe.json` + `/tmp/win95-screen.png`, detects SUCCESS vs FAIL modes, optionally drives keyboard input. |\n| `src/renderer/smb/index.ts` | Wraps `console.log` so `[smb]` and `[nbns]` lines tee to `$TMPDIR/windows95-smb.log` (outside Electron, readable by any polling script — no CDP needed). |\n| `tools/probe-boot.sh` | One-shot: kill leftovers → parcel build → launch Electron → poll `/tmp/win95-probe.done` → report → kill. |\n\n## Running from a git worktree\n\n`images/` is gitignored, so a fresh worktree has no disk image or default\nstate and every probe will fail at boot. Clone them from the main checkout\nfirst (APFS clonefile — instant, no extra disk space):\n\n```sh\nmkdir -p images\ncp -c \"$(git rev-parse --git-common-dir)/..\"/images/*.{img,bin} images/\n```\n\n## One-shot boot test\n\n```sh\ntools/probe-boot.sh\n```\n\nPrints SUCCESS or a FAIL verdict. ~40s on a clean run.\n\n## Boot + type into Run\n\n```sh\npkill -9 -f \"windows95.*electron\"; sleep 2\nrm -f \"$HOME/Library/Application Support/windows95/\"state-v*.bin\nrm -f /tmp/win95-probe.json /tmp/win95-probe.done \\\n      \"$TMPDIR/windows95-smb.log\"\n\nWIN95_PROBE=1 \\\nWIN95_PROBE_SCRIPT='HOST/HOST' \\\nWIN95_SMB_SHARE=\"$HOME/Downloads\" \\\n  ./node_modules/.bin/electron . > /tmp/win95-electron.log 2>&1 &\n```\n\n`WIN95_PROBE_SCRIPT='HOST/HOST'` types `\\\\HOST\\HOST` into Start → Run on\ndesktop. `WIN95_PROBE_DOSBOX=1` instead opens `command`, types `dir`,\nand (with `WIN95_PROBE_DOSBOX_ALTENTER=1`) toggles fullscreen — this is\nthe regression scenario for the windowed-DOS-box VBE leak.\n`WIN95_PROBE_CDROM=/path/to.iso` mounts an ISO on the secondary-IDE\nATAPI drive (bypasses the settings UI). `WIN95_PROBE_CDTRACE=1` logs\nevery secondary-channel ATA/ATAPI command to `/tmp/win95-cdtrace.log`.\n`WIN95_PROBE_VGATRACE=1` wraps the VGA I/O ports at the `io.ports[]`\nlayer and writes `[port, op, value, \"eip VMPE cplN\"]` tuples to\n`/tmp/win95-vgatrace.json` every tick (heavy — can hit 1M entries during\nboot). `/` → `\\` substitution (env var / shell quoting, pragmatism). The\nharness drives it via XT scancodes — Win95 doesn't have Win+R (Win98+\nonly), so the sequence is Esc, Esc, Ctrl+Esc, R, backslashes + text,\nEnter.\n\n## Reading results\n\n| File | What |\n|---|---|\n| `/tmp/win95-probe.json` | Live status: `phase` (`init`/`text-mode`/`splash`/`desktop`), `gfxW/H`, `textScreen`, `instructionDelta`, `verdict` |\n| `/tmp/win95-probe.done` | Written once when verdict is decided |\n| `/tmp/win95-screen.png` | Canvas screenshot, refreshed each tick |\n| `$TMPDIR/windows95-smb.log` | SMB/NBNS protocol trace |\n| `/tmp/win95-electron.log` | Electron stderr |\n\n## Verdicts\n\n| Verdict | Meaning | Action |\n|---|---|---|\n| `SUCCESS` | Canvas ≥640×480, CPU active, uptime >30s | desktop reached |\n| `FAIL_VXDLINK` | \"Invalid VxD dynamic link call\" | flaky — retry |\n| `FAIL_IOS` / `FAIL_PROTECTION` | IOS subsystem protection error | usually driver/BIOS mismatch |\n| `FAIL_KRNL386` | \"Cannot find KRNL386.EXE\" in safe mode | disk reads returning garbage — wasm/BIOS drift |\n| `FAIL_SPLASH_HANG` | Canvas stuck 320×400 for >70s | IRQ starvation — if you're on v86 master, check the IDE register fix |\n| `FAIL_HUNG` | CPU stopped advancing or text screen frozen 40s | hard hang |\n\n## Rules of the road\n\n- **Sporadic bluescreens are normal** on all v86 versions. One FAIL_VXDLINK\n  or FAIL_HUNG doesn't prove anything — retry up to 3×.\n- **Always clean state** (`state-v*.bin` — the suffix tracks `STATE_VERSION`\n  in `src/constants.ts`, so never hardcode a version) before a probe. `pkill`\n  on a wedged Electron triggers `onbeforeunload`, saving the *corrupted*\n  state. Deleting it forces fallback to `images/default-state.bin`.\n- **Don't trust the text buffer in graphics mode.** After desktop (≥640×480)\n  the stale BIOS text lingers in the buffer. The harness's `phase` field\n  accounts for this; don't re-read `textScreen` in a `desktop` phase and\n  think you hit a BSOD.\n- **Kill Electron when done.** Background processes pile up, each holding\n  the disk image lock. `pkill -f \"windows95.*electron\"` on every path out.\n\n## Bisecting v86\n\n`tools/bisect-v86.sh <commit>` handles one step. The harness retries 3×\nper commit. Hard-won lessons:\n\n1. **Validate bounds against a known-good binary.** Source-built wasm can\n   drift from prod due to cargo/rustc version differences. We hit this:\n   the \"GOOD\" bound produced a wasm that couldn't read the disk at all.\n2. **JS-only when toolchain drifts.** Keep the prod wasm, rebuild only\n   libv86.js at each commit. Closure is deterministic enough; cargo\n   isn't always. Works until you cross a commit that changes the JS↔wasm\n   ABI (for v86, the APIC→Rust port in Aug 2025).\n3. **Retry on FAIL, never on SUCCESS.** One SUCCESS = commit is good.\n   Three different FAILs at the same commit = commit is bad.\n4. **State cleanup between runs** (see above). Skipping this is the #1\n   cause of spurious \"bad\" verdicts during bisect.\n\n## Extending the harness\n\n- New verdicts: add to the chain in `collectStatus` in `debug-harness.ts`\n- New keyboard actions: extend `runScript` (current types: `keys`, `chord`,\n  `text`, `wait`)\n- New probe signals: add to `ProbeStatus` interface\n\nGate everything new on `process.env.WIN95_PROBE === \"1\"` so it stays out\nof the normal app.\n\n## Common failure diagnostics\n\n| Symptom | Check |\n|---|---|\n| No SMB traffic at all | `$TMPDIR/windows95-smb.log` should have `hooked adapter` line. If absent, v86 API changed — see `src/renderer/smb/README.md` |\n| SMB hooks fire, no connection | Win95's \"NetBIOS over TCP/IP\" checkbox — bake into default-state.bin |\n| Boot hangs on `2996c087` or older v86 | You probably have a ABI-mismatched wasm/JS pair. Prod wasm is the ground truth; rebuild JS against it. |\n\n## VXDLINK: flake vs. real bug\n\nTwo different things produce FAIL_VXDLINK:\n\n1. **Sporadic flake** (~1 in 2–3 runs even on known-good images): passes on\n   retry. This is why the retry-3× rule exists.\n2. **Deterministic failure** (same address every run, e.g.\n   `VMM(01)+000036E5 → device \"C000\" service E3E4`): the image's disk\n   layout triggers a real v86 disk-path bug. Known triggers: zeroing free\n   space via the \"mcopy a giant zero file, then delete it\" trick, and\n   offline (mtools) mass-deletion of recently-written file trees. The same\n   image boots fine in QEMU and passes fsck. Retrying never helps; the\n   image content must change.\n\nThis is the canonical verdict-interpretation policy (other docs link here):\n\n- **One SUCCESS** = the image can boot. Good — same rule as bisecting.\n- **Three identical failures** (same VxD address) = the image is in a bad\n  state. Stop retrying; the content must change.\n- Anything in between = keep retrying, you are looking at flakes.\n\nNever conclude anything from a single FAIL.\n\n## Probing the state-restore path\n\n`WIN95_PROBE_RESTORE=1 tools/probe-boot.sh` makes the probe restore state\n(user state → `images/default-state.bin` fallback) instead of cold\nbooting. Use it to verify a freshly generated default-state.bin actually\nresumes to the desktop — required after every image change.\n"},"items":[{"name":"SKILL.md","path":".claude/skills/inspect-disk/SKILL.md","title":"inspect-disk Skill","content":"---\nname: inspect-disk\ndescription: Inspect or modify the Windows 95 disk image offline with mtools — no QEMU, no Electron, no boot. Use when checking what's on the C: drive, extracting/reading files from the image, copying files into it, or verifying image contents after a change.\n---\n\n# Inspecting windows95.img without booting it\n\n`images/windows95.img` is a 1 GB raw MBR disk with a single FAT32 (0x0C)\npartition starting at sector 63. mtools (installed via Homebrew) can read\nand write it directly — the only trick is the partition byte offset:\n\n```\n63 sectors × 512 bytes = 32256\n```\n\nEvery command takes the same image spec: `-i images/windows95.img@@32256 ::`\n\n## Read operations (always safe)\n\n| Task | Command |\n|---|---|\n| List root | `mdir -i images/windows95.img@@32256 ::` |\n| List a subdir | `mdir -i images/windows95.img@@32256 ::/WINDOWS` |\n| Recursive bare listing | `mdir -/ -b -i images/windows95.img@@32256 ::` |\n| Print a text file | `mtype -i images/windows95.img@@32256 ::/AUTOEXEC.BAT` |\n| Extract a file to host | `mcopy -i images/windows95.img@@32256 ::/CONFIG.SYS /tmp/` |\n| Extract a dir recursively | `mcopy -s -i images/windows95.img@@32256 ::/TOOLS /tmp/tools` |\n| Filesystem info | `minfo -i images/windows95.img@@32256 ::` |\n| Disk usage (in 4 KB clusters) | `mdu -i images/windows95.img@@32256 ::` |\n\nLong filenames work (`PROGRA~1` ↔ `Program Files`); quote paths with\nspaces. Paths use `::/` as the root of C:.\n\n**Hidden files need `-a`.** Plain `mdir` silently skips hidden/system files\n(`BOOTLOG.TXT`, `SYSTEM.DA0`, `ShellIconCache`, `ie5bak.DAT`, …). Existence\nchecks must use `mdir -a`, or you'll wrongly conclude a file isn't there.\n\n**Check the image isn't in use first.** A running Electron *or QEMU* session\nholds the image open and writes to it lazily (Win95's VCACHE flushes on its\nown schedule):\n\n```sh\nlsof images/windows95.img   # must come back empty\n```\n\nA copy taken while the VM runs will have FAT inconsistencies (directory\nentries written, FAT not yet flushed). To repair a copy:\n\n```sh\nDEV=$(hdiutil attach -imagekey diskimage-class=CRawDiskImage -nomount copy.img | head -1 | awk '{print $1}')\nfsck_msdos -y \"${DEV}s1\"\nhdiutil detach \"$DEV\"\n```\n\n## Write operations (read the warnings first)\n\n| Task | Command |\n|---|---|\n| Copy file into image | `mcopy -o -i images/windows95.img@@32256 /host/file.txt ::/TOOLS/` |\n| Make a directory | `mmd -i images/windows95.img@@32256 ::/NEWDIR` |\n| Delete a file | `mdel -i images/windows95.img@@32256 ::/FILE.TXT` |\n| Delete a dir recursively | `mdeltree -i images/windows95.img@@32256 ::/DIR` |\n| Clear R/S/H attributes | `mattrib -i images/windows95.img@@32256 -r -s -h ::/FILE` (`-/` for recursive) |\n\n`mdel`/`mdeltree` fail on read-only/hidden/system files — run `mattrib`\nfirst. Note that bulk offline cleanup is currently discouraged: offline\nmodification worsens v86 cold-boot reliability. Prefer doing cleanup\ninside Windows; see \"Slimming the image\" in `docs/qemu.md`.\n\n### Warning 1: never write while a VM holds the image\n\nIf Electron/v86 or QEMU is running against the image, offline writes race\nthe guest's own writes and corrupt the FAT. Check first:\n\n```sh\npgrep -fl \"windows95.*electron|qemu.*windows95\"\n```\n\n### Warning 2: any offline write must be verified with the in-app probe\n\nQEMU and v86 exercise different boot paths (different hardware → different\ndriver init), and offline modification can break v86 cold boot while QEMU\nstill boots fine (\"Invalid VxD dynamic link call\" — see \"VXDLINK: flake\nvs. real bug\" in the probe-win95 skill). Verify with `tools/probe-boot.sh`,\nnot just `yarn run qemu`. In particular, **never zero free space via the\nmcopy-a-giant-zero-file trick** — that breaks v86 cold boot\ndeterministically; use `tools/zero-free-clusters.py` if you must zero free\nspace at all.\n\n### Warning 3: saved states cache the old disk\n\n`images/default-state.bin` and `~/Library/Application Support/windows95/state-v*.bin`\ncontain Win95's RAM, including VCACHE/FAT caches that reference the disk\n*as it was when the state was saved*. Modifying the disk offline and then\nresuming a saved state = filesystem corruption.\n\nAfter any offline write:\n\n```sh\nrm -f \"$HOME/Library/Application Support/windows95/\"state-v*.bin\n```\n\n…so the next boot is fresh (or from `default-state.bin`, which boots from\ndisk early enough to be safe only if the files you touched weren't open at\nstate-save time — when in doubt, do a full fresh boot and re-save).\n\n## Alternative: mount in Finder\n\n```sh\nhdiutil attach -imagekey diskimage-class=CRawDiskImage -readonly images/windows95.img\n# … browse /Volumes/WIN95 …\nhdiutil detach /Volumes/WIN95\n```\n\nRead-only is deliberate; use mtools for writes so you control exactly what\nchanges.\n\n## Worktree note\n\n`images/` is gitignored — in a fresh worktree there is no image to inspect.\nSee \"Running from a git worktree\" in the probe-win95 skill for the\nclone-from-main-checkout snippet.\n\n## Why not QEMU/v86?\n\nBooting to inspect the disk takes ~40s+ per round trip (see `probe-win95`)\nand every boot mutates the image (registry, swap, SCANDISK.LOG). mtools is\ninstant and, for read operations, leaves the image bit-identical — which\nmatters when you're trying to diff or bisect image changes.\n","category":".claude","tokens":1311},{"name":"SKILL.md","path":".claude/skills/update-v86/SKILL.md","title":"update-v86 Skill","content":"---\nname: update-v86\ndescription: Build and install v86 (wasm + libv86.js + BIOS) into windows95. Use when pulling upstream v86 changes, fixing a broken build, verifying the fork branches are still in sync, or setting up a fresh v86 checkout.\n---\n\n# Updating v86\n\nwindows95 builds v86 from source — not from copy.sh. Two small bugfix\npatches ride along on a fork branch until the upstream PRs land.\n\n## Sources\n\n| File | Built from |\n|---|---|\n| `src/renderer/lib/libv86.js` | `make build/libv86.js` in `../v86` |\n| `src/renderer/lib/build/v86.wasm` | `make build/v86.wasm` |\n| `bios/seabios.bin`, `bios/vgabios.bin` | copied from `../v86/bios/` |\n\n`tools/update-v86.js` runs those targets, copies the artifacts, runs 5\nsanity checks, and fails loudly if any prerequisite is missing. No\nfallbacks, no fetching from copy.sh.\n\n## The fork branch\n\nv86 should be checked out on **`felixrieseberg/v86:windows95-base`**.\nThat branch merges the feature branches tracked in\n[`docs/v86-patches.md`](../../../docs/v86-patches.md) — keep that table\nin sync with this list. Each is upstreamable on its own:\n\n- **`electron-renderer-fs-loader`** (PR #1540) — `src/lib.js` uses\n  `require(\"fs\")` instead of `await import(\"node:fs/promises\")`. Dynamic\n  import of `node:` URLs doesn't work in an Electron renderer.\n- **`ide-shared-registers`** (PR #1541) — `src/ide.js` writes ATA Command\n  Block registers (Features, Sector Count, LBA Low/Mid/High) to both\n  master and slave. Without this, Win95/98 hang at the splash screen on\n  any disk >~535MiB. Root cause: v86 commit `1b90d2e7` changed those\n  writes to target only `current_interface`, but per ATA spec they're\n  channel-shared (one register file on the IDE cable; both drives latch\n  the same value).\n- **`vmware-abspointer`** — `src/vmware.js` implements the VMware\n  backdoor (port `0x5658`): GETVERSION + ABSPOINTER_* so a guest driver\n  (VBADOS VBMOUSE) can read absolute cursor position and track the host\n  cursor 1:1 without pointer lock, and the legacy text-clipboard commands\n  6–9 so `W95TOOLS.EXE` (guest-tools/agent) can sync `CF_TEXT` with\n  the host. Consumes `mouse-absolute` and `vmware-clipboard-host` bus\n  events; emits `vmware-absolute-mouse` and `vmware-clipboard-guest`.\n- **`vmware-gettime`** *(stacked on the clipboard branch)* —\n  `src/vmware.js` adds backdoor command GETTIME (23): EAX = host UTC\n  seconds, EBX = microseconds, ECX = max time lag, EDX = host UTC offset\n  in minutes. `W95TOOLS.EXE` polls it and calls `SetLocalTime`, so a\n  resumed guest (which never re-reads the RTC) snaps back to host time.\n- **`fake-network-copy-tcp-addrs`** — `src/browser/fake_network.js`\n  copies the four address subarrays (`hsrc/hdest/psrc/pdest`) when a\n  `TCPConnection` is created from an inbound SYN. Upstream stores them\n  as zero-copy views into the NE2000 TX ring; once the guest's 12-slot\n  TX ring wraps (any concurrent traffic — SMB, NBNS, ping), `pump()`\n  builds segments with whatever IP now occupies that slot, the guest\n  RSTs them as belonging to no TCB, and `recv()` blocks forever.\n  Exercised by `tools/probe-tcp.sh`.\n- **`vga-defer-vbe-disable-v86`** — `src/vga.js` defers `dispi[4]=0`\n  written from V86 mode until a legacy attribute-mode write reaches the\n  hardware. Win9x's VDD virtualises ports 3B0–3DF for a windowed DOS VM\n  but not 1CE/1CF, so vgabios's VBE-disable leaks through while the rest\n  of its mode-set is captured into the VM's virtual register file —\n  without this the screen turns to planar garbage the moment you open a\n  DOS box.\n\n## Prerequisites\n\n```sh\nrustup target add wasm32-unknown-unknown\nbrew install openjdk\n# one-time: fetch the Closure compiler v86's Makefile pins to\ncurl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar \\\n  -o ../v86/closure-compiler/compiler.jar\n```\n\nClosure **must** be v20210601 — newer versions hit\n[closure-compiler#3972](https://github.com/google/closure-compiler/issues/3972)\non v86's source. The pin is in v86's Makefile.\n\n## Steps\n\n```sh\ncd ../v86\ngit fetch fork origin\ngit checkout windows95-base\ngit rebase fork/windows95-base   # in case fork was updated elsewhere\ncd ../windows95\nnode tools/update-v86.js\n```\n\nThat's it. Script runs both `make` targets, copies, verifies.\n\n## Sanity-check WARNs\n\nThe 5 checks assert invariants `src/renderer/smb/index.ts` and\n`tools/parcel-build.js` depend on. A WARN means upstream changed\nsomething load-bearing — don't ignore it:\n\n1. **`await import(\"node:...\")` still present** → PR #1540 was reverted\n   or the pattern moved. Electron renderer will fail to load disk images.\n2. **`master.features_reg=` missing in minified** → PR #1541 was reverted\n   or `windows95-base` lost the commit. Win95 will hang at splash on\n   disks >535MiB. Check `cd ../v86 && git log --oneline windows95-base`.\n3. **Export pattern changed** → `tools/parcel-build.js` shim needs\n   updating. Look for `module.exports.V86=` and `window.V86=`.\n4. **`tcp-connection` event gone** → SMB falls back to the old-API theft\n   hack in `src/renderer/smb/index.ts` — still works, but surprising.\n5. **`on_tcp_connection` gone** → old-API fallback is dead. SMB integration\n   only works via the `tcp-connection` bus event now. Harmless; update\n   the comment in `index.ts` and retire the theft code.\n\n## After updating, probe-test\n\n```sh\nnode tools/update-v86.js && tools/probe-boot.sh\n```\n\nShould land SUCCESS in ~40s. If FAIL_SPLASH_HANG, the IDE fix didn't\ntake — check `grep master.features_reg src/renderer/lib/libv86.js`. If\nFAIL_VXDLINK, retry — sporadic bluescreens are normal (see the\n`probe-win95` skill).\n\n## When a PR merges upstream\n\nRebase `windows95-base` to drop the now-redundant commit:\n\n```sh\ncd ../v86\ngit fetch origin\ngit checkout windows95-base\ngit rebase origin/master              # drops the merged commit cleanly\ngit push fork windows95-base --force-with-lease\n```\n\nIf **both** PRs are upstream, retire the fork branch entirely:\n\n1. Point `tools/update-v86.js` default at `origin/master` (it already\n   uses `../v86`, so just `git checkout master` there)\n2. Delete `fork/windows95-base`\n3. Remove this skill's \"The fork branch\" section\n4. Confirm the 5 sanity checks still pass — they're version-agnostic\n\n## Integration contract with SMB\n\nThe SMB server sits on top of v86's network adapter. Details in\n`src/renderer/smb/README.md`. Short version: the new path uses the\n`tcp-connection` bus event; the fallback path uses\n`adapter.on_tcp_connection` callback + connection-theft (stealing a\n`TCPConnection` the HTTP probe builds for us). Both use `.on_data` on\nthe conn, not `.on(\"data\")`, because Closure dead-code-eliminates the\nevent emitter plumbing.\n\nIf any v86 update breaks these assumptions, `src/renderer/smb/index.ts`\nneeds updating, not just `tools/update-v86.js`.\n","category":".claude","tokens":1706},{"name":"SKILL.md","path":".claude/skills/probe-win95/SKILL.md","title":"probe-win95 Skill","content":"---\nname: probe-win95\ndescription: Boot Windows 95 in Electron under Claude's control, without a human clicking anything. Use when testing v86 updates, SMB changes, keyboard input, boot stability, or bisecting regressions.\n---\n\n# Probing Windows 95 autonomously\n\nYou can run and test the Win95 VM yourself. The harness is already wired\nup — three pieces:\n\n| File | Role |\n|---|---|\n| `src/renderer/debug-harness.ts` | Activated by `WIN95_PROBE=1`. Boots fresh automatically, samples CPU + VGA + text screen every 5s, writes `/tmp/win95-probe.json` + `/tmp/win95-screen.png`, detects SUCCESS vs FAIL modes, optionally drives keyboard input. |\n| `src/renderer/smb/index.ts` | Wraps `console.log` so `[smb]` and `[nbns]` lines tee to `$TMPDIR/windows95-smb.log` (outside Electron, readable by any polling script — no CDP needed). |\n| `tools/probe-boot.sh` | One-shot: kill leftovers → parcel build → launch Electron → poll `/tmp/win95-probe.done` → report → kill. |\n\n## Running from a git worktree\n\n`images/` is gitignored, so a fresh worktree has no disk image or default\nstate and every probe will fail at boot. Clone them from the main checkout\nfirst (APFS clonefile — instant, no extra disk space):\n\n```sh\nmkdir -p images\ncp -c \"$(git rev-parse --git-common-dir)/..\"/images/*.{img,bin} images/\n```\n\n## One-shot boot test\n\n```sh\ntools/probe-boot.sh\n```\n\nPrints SUCCESS or a FAIL verdict. ~40s on a clean run.\n\n## Boot + type into Run\n\n```sh\npkill -9 -f \"windows95.*electron\"; sleep 2\nrm -f \"$HOME/Library/Application Support/windows95/\"state-v*.bin\nrm -f /tmp/win95-probe.json /tmp/win95-probe.done \\\n      \"$TMPDIR/windows95-smb.log\"\n\nWIN95_PROBE=1 \\\nWIN95_PROBE_SCRIPT='HOST/HOST' \\\nWIN95_SMB_SHARE=\"$HOME/Downloads\" \\\n  ./node_modules/.bin/electron . > /tmp/win95-electron.log 2>&1 &\n```\n\n`WIN95_PROBE_SCRIPT='HOST/HOST'` types `\\\\HOST\\HOST` into Start → Run on\ndesktop. `WIN95_PROBE_DOSBOX=1` instead opens `command`, types `dir`,\nand (with `WIN95_PROBE_DOSBOX_ALTENTER=1`) toggles fullscreen — this is\nthe regression scenario for the windowed-DOS-box VBE leak.\n`WIN95_PROBE_CDROM=/path/to.iso` mounts an ISO on the secondary-IDE\nATAPI drive (bypasses the settings UI). `WIN95_PROBE_CDTRACE=1` logs\nevery secondary-channel ATA/ATAPI command to `/tmp/win95-cdtrace.log`.\n`WIN95_PROBE_VGATRACE=1` wraps the VGA I/O ports at the `io.ports[]`\nlayer and writes `[port, op, value, \"eip VMPE cplN\"]` tuples to\n`/tmp/win95-vgatrace.json` every tick (heavy — can hit 1M entries during\nboot). `/` → `\\` substitution (env var / shell quoting, pragmatism). The\nharness drives it via XT scancodes — Win95 doesn't have Win+R (Win98+\nonly), so the sequence is Esc, Esc, Ctrl+Esc, R, backslashes + text,\nEnter.\n\n## Reading results\n\n| File | What |\n|---|---|\n| `/tmp/win95-probe.json` | Live status: `phase` (`init`/`text-mode`/`splash`/`desktop`), `gfxW/H`, `textScreen`, `instructionDelta`, `verdict` |\n| `/tmp/win95-probe.done` | Written once when verdict is decided |\n| `/tmp/win95-screen.png` | Canvas screenshot, refreshed each tick |\n| `$TMPDIR/windows95-smb.log` | SMB/NBNS protocol trace |\n| `/tmp/win95-electron.log` | Electron stderr |\n\n## Verdicts\n\n| Verdict | Meaning | Action |\n|---|---|---|\n| `SUCCESS` | Canvas ≥640×480, CPU active, uptime >30s | desktop reached |\n| `FAIL_VXDLINK` | \"Invalid VxD dynamic link call\" | flaky — retry |\n| `FAIL_IOS` / `FAIL_PROTECTION` | IOS subsystem protection error | usually driver/BIOS mismatch |\n| `FAIL_KRNL386` | \"Cannot find KRNL386.EXE\" in safe mode | disk reads returning garbage — wasm/BIOS drift |\n| `FAIL_SPLASH_HANG` | Canvas stuck 320×400 for >70s | IRQ starvation — if you're on v86 master, check the IDE register fix |\n| `FAIL_HUNG` | CPU stopped advancing or text screen frozen 40s | hard hang |\n\n## Rules of the road\n\n- **Sporadic bluescreens are normal** on all v86 versions. One FAIL_VXDLINK\n  or FAIL_HUNG doesn't prove anything — retry up to 3×.\n- **Always clean state** (`state-v*.bin` — the suffix tracks `STATE_VERSION`\n  in `src/constants.ts`, so never hardcode a version) before a probe. `pkill`\n  on a wedged Electron triggers `onbeforeunload`, saving the *corrupted*\n  state. Deleting it forces fallback to `images/default-state.bin`.\n- **Don't trust the text buffer in graphics mode.** After desktop (≥640×480)\n  the stale BIOS text lingers in the buffer. The harness's `phase` field\n  accounts for this; don't re-read `textScreen` in a `desktop` phase and\n  think you hit a BSOD.\n- **Kill Electron when done.** Background processes pile up, each holding\n  the disk image lock. `pkill -f \"windows95.*electron\"` on every path out.\n\n## Bisecting v86\n\n`tools/bisect-v86.sh <commit>` handles one step. The harness retries 3×\nper commit. Hard-won lessons:\n\n1. **Validate bounds against a known-good binary.** Source-built wasm can\n   drift from prod due to cargo/rustc version differences. We hit this:\n   the \"GOOD\" bound produced a wasm that couldn't read the disk at all.\n2. **JS-only when toolchain drifts.** Keep the prod wasm, rebuild only\n   libv86.js at each commit. Closure is deterministic enough; cargo\n   isn't always. Works until you cross a commit that changes the JS↔wasm\n   ABI (for v86, the APIC→Rust port in Aug 2025).\n3. **Retry on FAIL, never on SUCCESS.** One SUCCESS = commit is good.\n   Three different FAILs at the same commit = commit is bad.\n4. **State cleanup between runs** (see above). Skipping this is the #1\n   cause of spurious \"bad\" verdicts during bisect.\n\n## Extending the harness\n\n- New verdicts: add to the chain in `collectStatus` in `debug-harness.ts`\n- New keyboard actions: extend `runScript` (current types: `keys`, `chord`,\n  `text`, `wait`)\n- New probe signals: add to `ProbeStatus` interface\n\nGate everything new on `process.env.WIN95_PROBE === \"1\"` so it stays out\nof the normal app.\n\n## Common failure diagnostics\n\n| Symptom | Check |\n|---|---|\n| No SMB traffic at all | `$TMPDIR/windows95-smb.log` should have `hooked adapter` line. If absent, v86 API changed — see `src/renderer/smb/README.md` |\n| SMB hooks fire, no connection | Win95's \"NetBIOS over TCP/IP\" checkbox — bake into default-state.bin |\n| Boot hangs on `2996c087` or older v86 | You probably have a ABI-mismatched wasm/JS pair. Prod wasm is the ground truth; rebuild JS against it. |\n\n## VXDLINK: flake vs. real bug\n\nTwo different things produce FAIL_VXDLINK:\n\n1. **Sporadic flake** (~1 in 2–3 runs even on known-good images): passes on\n   retry. This is why the retry-3× rule exists.\n2. **Deterministic failure** (same address every run, e.g.\n   `VMM(01)+000036E5 → device \"C000\" service E3E4`): the image's disk\n   layout triggers a real v86 disk-path bug. Known triggers: zeroing free\n   space via the \"mcopy a giant zero file, then delete it\" trick, and\n   offline (mtools) mass-deletion of recently-written file trees. The same\n   image boots fine in QEMU and passes fsck. Retrying never helps; the\n   image content must change.\n\nThis is the canonical verdict-interpretation policy (other docs link here):\n\n- **One SUCCESS** = the image can boot. Good — same rule as bisecting.\n- **Three identical failures** (same VxD address) = the image is in a bad\n  state. Stop retrying; the content must change.\n- Anything in between = keep retrying, you are looking at flakes.\n\nNever conclude anything from a single FAIL.\n\n## Probing the state-restore path\n\n`WIN95_PROBE_RESTORE=1 tools/probe-boot.sh` makes the probe restore state\n(user state → `images/default-state.bin` fallback) instead of cold\nbooting. Use it to verify a freshly generated default-state.bin actually\nresumes to the desktop — required after every image change.\n","category":".claude","tokens":1901}]}