# Build spec: Wyse 3040 Tailscale exit-node appliance: unattended installer USB

> **For Claude Code.** Read this whole document before writing code. Build the repository described in §9 so that one command produces a bootable USB image. That USB must turn a Dell Wyse 3040 into a headless, self-maintaining Tailscale exit node with zero keyboard, monitor or SSH interaction. All tunable decisions live in `config.env` (§2); never hard-code them elsewhere. Where this spec says **VERIFY**, test the behaviour on the real target or in QEMU and adapt the implementation, then note what you found in `docs/FINDINGS.md`. Ask me before deviating from a **MUST**.

---

## 1. Goal and operating model

- **Hardware:** Dell Wyse 3040.
  - Intel Atom x5-Z8350 (Cherry Trail), 2 GB RAM.
  - 8 or 16 GB eMMC, which appears as `/dev/mmcblk0`.
  - UEFI only, Realtek gigabit Ethernet.
- **OS:** Debian 13 "trixie", amd64, minimal. No desktop, no "standard system utilities" task.
- **Purpose:** the appliance's only job is to run as a Tailscale exit node in the customer's own tailnet.
- **Customer experience:**
  1. Plug in Ethernet and power.
  2. Open `http://<hostname>.local` from a phone or laptop on the same home network.
  3. Click "Connect to Tailscale" and log in.
  4. In the Tailscale admin console, approve the exit node and disable key expiry.
  5. Never touch the device again.
- **Technician experience:**
  1. Set the BIOS (one-time checklist, §8).
  2. Insert the installer USB and power on.
  3. The device installs unattended and **powers off** when done.
  4. Remove the USB, label the device, ship it.
- **Production model:** the device ships logged out of Tailscale. No customer credentials are required. Optionally, a customer-specific auth key can be baked in (§5.6).
- **Operating principle:** after shipping, nobody ever logs in. Everything must self-heal or self-update:
  - automatic security and package updates, and automatic reboot when required;
  - tailscaled always restarted;
  - a hardware watchdog to recover from hangs;
  - automatic filesystem repair after power loss;
  - automatic return to onboarding mode if the Tailscale login is lost.

---

## 2. `config.env`: all decisions in one place (defaults chosen; I can override)

```bash
# --- Base OS ---
DEBIAN_RELEASE=trixie            # Debian 13
DEBIAN_ISO_URL=auto              # auto = latest debian-13.x.y-amd64-netinst.iso from cdimage.debian.org
DEBIAN_MIRROR=deb.debian.org
LOCALE=en_US.UTF-8
KEYMAP=us
TIMEZONE=Europe/Stockholm        # customers are in Sweden/Europe; controls the maintenance window

# --- Identity ---
HOSTNAME_PREFIX=wyse             # hostname = ${HOSTNAME_PREFIX}-<dell service tag, lowercase>
BRAND_NAME="Exit Node"           # shown on onboarding page
SUPPORT_URL=""                   # shown on onboarding page if set
SUPPORT_EMAIL=""

# --- Tailscale ---
TS_ADVERTISE_EXIT_NODE=true
TS_ACCEPT_DNS=false              # appliance keeps its own DNS; avoids resolver breakage
TS_ENABLE_SSH=false              # Tailscale SSH (customer-controlled via their ACL); off by default
TS_AUTO_UPDATE=true              # `tailscale set --auto-update`
TS_AUTHKEY=""                    # OPTIONAL: if set, device auto-joins at first boot (per-customer build, §5.6)
TS_EXTRA_TAGS=""                 # e.g. tag:exitnode — only meaningful with TS_AUTHKEY

# --- Maintenance ---
UPDATE_REBOOT_TIME=04:00         # local time; reboot only if an update requires it
WEEKLY_REBOOT=false              # optional extra hygiene reboot, Sunday at UPDATE_REBOOT_TIME
WATCHDOG=true                    # systemd hardware watchdog (iTCO_wdt), softdog fallback

# --- Hardening / reliability ---
FIREWALL=true                    # nftables default-deny inbound (see §5.8)
ZRAM_SWAP=true                   # small zram swap, no disk swap
JOURNAL_VOLATILE=true            # logs in RAM only (protects eMMC)
CSTATE_WORKAROUND=true           # adds intel_idle.max_cstate=1 (Cherry Trail freeze mitigation)
SAFETY_CHECK_PRODUCT="3040"      # installer refuses to run unless DMI product_name contains this

# --- Bench / debug (MUST be false for customer builds) ---
DEBUG_BUILD=false
DEBUG_SSH_PUBKEY=""              # if DEBUG_BUILD=true: openssh-server + this key for user `wyseadmin`
```

The build MUST refuse to produce an image if `DEBUG_BUILD=true` unless `--debug` is passed explicitly. Debug images MUST get a different filename, e.g. `wyse-installer-DEBUG.img`.

---

## 3. Approach and deliverables

**Approach:** remaster the official Debian 13 netinst ISO.
- Inject a fully automated **preseed** that answers every installer question.
- Copy a **provisioning payload** (scripts, systemd units, configs, onboarding web app) onto the ISO.
- `preseed/late_command` copies the payload into the target and runs `install.sh` inside it.
- The installer needs internet via the Ethernet cable during install (DHCP). It pulls packages from the Debian mirror and `pkgs.tailscale.com`.

**Deliverables:**
1. `build.sh`:
   - downloads the ISO and verifies it with `SHA512SUMS` and its GPG signature, using the Debian CD signing keys;
   - renders the templates from `config.env`;
   - remasters the ISO with `xorriso`, keeping it hybrid so it is `dd`-able to USB and UEFI-bootable;
   - outputs `out/wyse-installer-<version>.iso` plus a `.sha256`.
   - It runs inside a Docker container (`debian:trixie`) so it works identically on macOS and Linux build hosts.
2. `tools/write-usb.sh`: writes the image to a USB stick.
   - Lists removable disks only and requires typing the device name to confirm.
   - Refuses internal or system disks.
3. `tools/make-reset-stick.sh`: formats a stick as FAT32 with label `WYSERESET` and a marker file (§5.9).
4. `tests/qemu-test.sh`: end-to-end test in QEMU (§10).
5. `README.md` covering technician workflow, build instructions, and the customer onboarding card text (§7).
6. `docs/FINDINGS.md`: results of every **VERIFY** item.

**Optional phase 2** (do not build now; keep the design compatible):
- a "golden image + flasher USB" variant for faster throughput, about 3 min per unit and offline;
- it would reuse the same `payload/` by applying it to a debootstrapped image instead of via d-i.

---

## 4. Preseed requirements (installer stage)

The installer MUST run with **zero prompts**. Boot-menu changes:
- the default entry is the automated install with `auto=true priority=critical`;
- the GRUB timeout is about 5 s;
- the 3040 is UEFI-only, so edit `boot/grub/grub.cfg` (also isolinux for completeness).

Preferred: embed `preseed.cfg` in the initrd, so it is available before the CD-ROM is mounted. **VERIFY** which method is reliable on the 3040.

**early_command safety interlock (MUST):**
- Abort with a clear on-screen error and halt unless all of these hold:
  - `/sys/class/dmi/id/product_name` contains `${SAFETY_CHECK_PRODUCT}`;
  - `/dev/mmcblk0` exists;
  - the installer USB is not `mmcblk0`.
- This prevents a stray stick from wiping a PC.
- **VERIFY** the exact DMI product string on a real unit.
- The QEMU test sets an override via a kernel parameter, `wyse.skipcheck=1`, which is honoured only in debug builds.

**Key preseed settings (implement all, adjust syntax as needed):**
- **Localization:** locale, keymap and timezone from config. Set the clock to UTC with NTP on.
- **Network:** `netcfg/choose_interface auto` with DHCP.
  - Set a hostname placeholder; the real hostname is set in `install.sh`.
  - Use no domain.
- **Mirror:** `${DEBIAN_MIRROR}`, HTTP, no proxy.
- **Accounts:**
  - The root login is disabled.
  - Create user `wyseadmin` with a **locked** password.
  - Debug builds get an SSH key only, set later in `install.sh`.
  - No usable console or SSH password may exist.
- **Partitioning:** wipe `/dev/mmcblk0` entirely with a GPT label and no swap partition.
  - ESP: 256 MB FAT32, mounted at `/boot/efi`.
  - Root: ext4 on the rest of the disk.
  - Root mount options: `noatime,errors=remount-ro,commit=30`.
  - Remove any existing LVM, RAID or partitions from ThinOS, ThinLinux or Windows IoT without asking.
  - Never touch `mmcblk0boot0/1` or `mmcblk0rpmb`.
- **Packages:** no tasksel tasks (not even "standard"). `pkgsel/include` adds:
  - `ca-certificates curl gnupg nftables avahi-daemon python3 qrencode jq ethtool unattended-upgrades apt-listchanges needrestart zram-tools systemd-resolved systemd-timesyncd` (systemd-resolved is a separate package in trixie);
  - `openssh-server` only when `DEBUG_BUILD=true`.
  - Disable popularity-contest.
- **Bootloader:** GRUB EFI with `grub-installer/force-efi-extra-removable true`, so the fallback `EFI/BOOT/BOOTX64.EFI` path exists. Boot must not depend on NVRAM entries surviving.
- **Kernel command line:** `fsck.repair=yes`, plus `intel_idle.max_cstate=1` when `CSTATE_WORKAROUND=true`.
- **late_command:** copy `/cdrom/wyse/` (the payload) to `/target/opt/wyse/`, then run `in-target /opt/wyse/install.sh`.
  - If `install.sh` exits non-zero, show a clear on-screen failure and do NOT power off. That way a failed unit is visibly different from a good one.
- **Finish:** **power off** instead of rebooting (`debian-installer/exit/poweroff true`). Power-off is the technician's "done" signal and avoids reboot loops back into the USB.

---

## 5. Provisioning payload (`payload/install.sh`, runs in-target)

It must be idempotent, use `set -euo pipefail`, and log to `/var/log/wyse-install.log`. Each subsection below is a separate, well-named function.

### 5.1 Identity
- Add a first-boot oneshot, `wyse-firstboot.service`, that sets the hostname at first boot rather than during install:
  - read the Dell service tag (`/sys/class/dmi/id/product_serial` or `dmidecode -s system-serial-number`);
  - lowercase and sanitize it;
  - set the hostname to `${HOSTNAME_PREFIX}-<tag>`;
  - fall back to the last 6 hex digits of the primary MAC if no tag exists;
  - **VERIFY** where the 3040 exposes the service tag.
- Regenerate `/etc/machine-id` at first boot: truncate it to empty at install time.
- Avahi publishes `<hostname>.local`. Ensure `publish-addresses=yes` and that the service is enabled.

### 5.2 Networking
- Replace ifupdown with **systemd-networkd**: a `.network` file matching `Type=ether`, with `DHCP=yes` and IPv6 RA accepted.
  - Any NIC name works, and every unit gets an identical config.
  - Purge `ifupdown` and empty `/etc/network/interfaces`.
- Enable `systemd-resolved` (stub resolver) and `systemd-timesyncd`. Correct time is required for TLS and Tailscale.
- Do not configure Wi-Fi; blacklist Wi-Fi modules if present.
- `systemd-networkd-wait-online` MUST NOT block boot indefinitely: use `--any` and a timeout of about 30 s.

### 5.3 Exit-node kernel and NIC tuning
- In `/etc/sysctl.d/99-wyse-exitnode.conf`:
  - `net.ipv4.ip_forward=1`
  - `net.ipv6.conf.all.forwarding=1`
- UDP GRO forwarding, as Tailscale recommends for Linux exit nodes:
  - a oneshot unit, `wyse-ethtool.service`, runs `ethtool -K <iface> rx-udp-gro-forwarding on rx-gro-list off` on the interface carrying the default route;
  - it triggers after `network-online.target` and also via a networkd hook when the link changes;
  - **VERIFY** the Realtek driver supports it; failure must be non-fatal.

### 5.4 Tailscale
- Add Tailscale's official apt repo for Debian trixie using a signed-by keyring, then install `tailscale`.
- Add a drop-in at `/etc/systemd/system/tailscaled.service.d/override.conf` with `Restart=always`, `RestartSec=5`, and `StartLimitIntervalSec=0` (in `[Unit]`).
- First-boot preferences, set before login so the customer only has to log in:
  - `--advertise-exit-node` when `TS_ADVERTISE_EXIT_NODE=true`;
  - `--hostname=<hostname>`;
  - `--accept-dns=${TS_ACCEPT_DNS}`;
  - `--ssh` when `TS_ENABLE_SSH=true`;
  - `tailscale set --auto-update` when `TS_AUTO_UPDATE=true`.
- **VERIFY** whether `tailscale set …` persists these preferences while logged out. If it doesn't, use `tailscale up <flags> --timeout=5s` (or equivalent), which stores the preferences and initiates login without blocking. Document what works.
- Disable key-expiry warnings on our side; that's a customer-console action. If the node ever returns to `NeedsLogin` (expiry, logout, reset), the onboarding page MUST come back automatically (§5.5).

### 5.5 Onboarding web page (the customer-facing UI)
- A tiny app in **Python 3 stdlib only**, no pip: `payload/onboarding/app.py`.
  - It runs as a systemd service, `wyse-onboarding.service`, on **TCP 80**, LAN-facing.
  - It runs as a dedicated unprivileged user, with `CAP_NET_BIND_SERVICE` and sandboxing (`ProtectSystem=strict` and similar).
- The app reads state from `tailscale status --json`: `BackendState` and `AuthURL`. Give it access via sudoers-free means. **VERIFY** whether the tailscaled LocalAPI socket permissions or `tailscale --socket` need an operator setting, e.g. `tailscale set --operator=<svcuser>`.
- **Page states:**
  - **`NeedsLogin` / `NoState`:**
    - show the brand, hostname, and a big **"Connect to Tailscale"** button linking to `AuthURL`, plus a QR code of the URL (inline SVG via `qrencode`) for scanning from another device;
    - if `AuthURL` is empty or stale, trigger a fresh login via the LocalAPI or CLI with a non-blocking timeout, then refresh;
    - poll every 3 s and auto-advance when the state changes.
  - **`Running`:**
    - show "Connected to <tailnet>" plus the checklist: (1) approve the exit node and (2) disable key expiry, each with a direct link to `https://login.tailscale.com/admin/machines`;
    - then (3) select this device as the exit node in the Tailscale app;
    - show whether the exit node is approved, if detectable from status. **VERIFY**.
  - **Any error:** show a plain-language message plus `SUPPORT_URL` / `SUPPORT_EMAIL`.
- **No controls when `Running`.** The page is status-only: no logout, no reset, no settings. This means anyone on the LAN can see the status but change nothing.
- Simple, mobile-first HTML with inline CSS and no external assets, so it works offline on the LAN.

### 5.6 Optional pre-join with auth key (per-customer builds)
- If `TS_AUTHKEY` is set, first boot runs `tailscale up --auth-key=… --advertise-exit-node --hostname=… [--advertise-tags=${TS_EXTRA_TAGS}]`.
- Then shred the key file.
- The onboarding page then simply shows `Running`.
- The key MUST NOT appear in logs or the journal.

### 5.7 Automatic updates and reboots
- `unattended-upgrades` with these origins:
  - Debian security, Debian updates, and Debian point releases for trixie;
  - the Tailscale repo origin. **VERIFY** the exact `Origin` / `Label` from `apt-cache policy`.
- Configuration:
  - `Automatic-Reboot "true"`
  - `Automatic-Reboot-WithUsers "true"`
  - `Automatic-Reboot-Time "${UPDATE_REBOOT_TIME}"`
  - `Remove-Unused-Kernel-Packages "true"`
  - `Remove-Unused-Dependencies "true"`
  - `APT::Periodic::AutocleanInterval "7"` (the eMMC is small)
- `needrestart` in automatic mode (`$nrconf{restart} = 'a'`), so services restart after library updates without prompts.
- Make the apt timers fire daily, with randomized delay.
- If `WEEKLY_REBOOT=true`: a systemd timer that reboots Sunday at `UPDATE_REBOOT_TIME`.
- **Out of scope:** Debian major-version upgrades (13→14). Document this in the README as a future fleet task. Debian 13 has security support to about 2028 and LTS to about 2030.

### 5.8 Firewall (nftables, when `FIREWALL=true`)
- **Input** policy drop. Accept:
  - `lo` and established/related traffic;
  - ICMP and ICMPv6, including neighbour discovery and RA;
  - DHCPv4 and DHCPv6 client replies;
  - mDNS 5353/udp from the LAN;
  - TCP 80 from RFC1918, link-local and ULA sources only;
  - UDP 41641 (Tailscale direct connections);
  - everything on `tailscale0`;
  - TCP 22 only in debug builds.
- **Forward:** must not break exit-node routing. Allow `tailscale0` → LAN/WAN and established return traffic.
- **Coexistence:** Tailscale manages its own nftables/iptables rules, so make sure ours coexist. **VERIFY** by testing exit-node traffic end-to-end with the firewall on (§10).

### 5.9 Self-healing and resilience
- **Watchdog:** in `/etc/systemd/system.conf.d/`, set `RuntimeWatchdogSec=30s` and `RebootWatchdogSec=10min`.
  - Load `iTCO_wdt`, falling back to `softdog` if no hardware watchdog is found.
  - **VERIFY** that `/dev/watchdog` works on the 3040.
- **Filesystem:** `fsck.repair=yes` on the kernel command line, plus ext4 with `errors=remount-ro`.
  - Add a boot-time check: if root came up read-only, reboot once to trigger fsck.
- **Journal:** `Storage=volatile`, `RuntimeMaxUse=32M`. Keep `/var/log/wyse-install.log` on disk.
- **zram:** `zram-tools` at about 25% of RAM, lz4 or zstd.
- **Health check:** `wyse-health.timer` runs every 5 min.
  - If `tailscale status` fails for 3 consecutive runs, restart tailscaled.
  - If the default route has been missing for over 10 minutes, restart systemd-networkd.
  - Log one line per action.
  - No reboot loops: at most 1 health-triggered reboot per 6 h, tracked in `/var/lib/wyse/`.
- **Factory reset via USB stick:**
  - A udev rule plus service, and also a check at boot.
  - On detecting a FAT32 volume labelled `WYSERESET` that contains `RESET.txt`:
    - run `tailscale logout` and wipe `/var/lib/tailscale/` except what tailscaled needs to restart cleanly;
    - re-apply the first-boot preferences and return to onboarding mode;
    - write `DONE-<hostname>-<timestamp>.txt` to the stick, then reboot.
  - This is the only "maintenance" a customer or support person ever does.
- **Console status screen:** a no-keyboard fallback onboarding route.
  - tty1 runs a service that redraws every 10 s: hostname, LAN IP, Tailscale state, and, when `NeedsLogin`, a terminal QR code of the `AuthURL` (`qrencode -t ANSIUTF8`) plus the onboarding URL.
  - No getty login prompt on tty1. Other ttys keep getty, but no password works anyway.

### 5.10 Footprint and cleanup
- Purge everything not needed:
  - `installation-report`, `laptop-detect`, and similar;
  - `ifupdown`;
  - man pages are optional; ask if unsure.
- `apt clean` at the end.
- Target installed size: **< 1.5 GB**, and idle RAM **< 200 MB**. Report the actual numbers in FINDINGS.md.

---

## 6. Security requirements (MUST)
- No customer credentials anywhere in the image. `TS_AUTHKEY` exists only in explicitly per-customer builds.
- No password-based login anywhere: console, SSH or web.
- No SSH server in customer builds. Tailscale SSH is off unless `TS_ENABLE_SSH=true`.
- The onboarding page is read-only once `Running`, has no shell-outs with user input, and is bound to port 80 on the LAN with the firewall restriction.
- The ISO's integrity is verified at build time, and outputs ship with a SHA-256.
- Secure Boot: **VERIFY** that the image boots with Secure Boot **enabled** using Debian's signed shim and GRUB. If it does, the BIOS checklist keeps Secure Boot on.

---

## 7. Customer onboarding card (text for README, to be printed)

1. Connect the device to your router with the cable, then plug in power. Wait 2 minutes.
2. On a phone or laptop **on your home Wi-Fi**, open **http://wyse-XXXXXX.local** (printed on the sticker), or scan the QR code.
3. Tap **Connect to Tailscale** and log in with your Tailscale account.
4. In the Tailscale admin console (link on the page), find the device. Then:
   - **Edit route settings → Use as exit node** (allow it);
   - **⋯ → Disable key expiry**.
5. In the Tailscale app on your phone or laptop: **Exit node → wyse-XXXXXX**.

If the `.local` address doesn't open, find the device's IP address in your router's app and open `http://<that-IP>`.

Also write `tools/make-stickers.py`. Its input is a CSV of service tags. Its output is a print-ready PDF or HTML of labels with the hostname, URL and QR code.

---

## 8. Technician BIOS checklist (manual, once per device; document in README)
- Enter setup with F2 (one-time boot menu: F12). The factory BIOS password is often `Fireport`; replace it with our own admin password.
- Update the BIOS to Dell's latest for the 3040 before the first production batch. **VERIFY** the version used.
- **Boot:**
  - UEFI mode;
  - USB boot enabled;
  - boot order: internal eMMC first, then USB. The one-time F12 menu is used for installing.
- **Secure Boot:** per §6 result.
- **Power management → AC Recovery = Power On.** This is **critical**: the unit restarts by itself after a power cut.
- Disable PXE / network boot, unused Wi-Fi and Bluetooth, and Wake-on-LAN.
- **VERIFY** whether Dell's tooling can apply these BIOS settings in bulk, e.g. by cloning a BIOS config, and document it if so.

---

## 9. Repository layout

```
wyse-exitnode/
├── config.env
├── build.sh                      # docker wrapper → scripts/build-inner.sh
├── scripts/build-inner.sh        # download, verify, render, remaster
├── preseed/preseed.cfg.tmpl
├── payload/
│   ├── install.sh
│   ├── lib/*.sh                  # one file per §5 subsection
│   ├── systemd/*.service|*.timer|*.conf
│   ├── network/10-ether.network
│   ├── nftables.conf.tmpl
│   ├── apt/50unattended-upgrades.tmpl, 20auto-upgrades
│   ├── onboarding/app.py, templates (inline)
│   └── console/status.sh
├── tools/write-usb.sh, make-reset-stick.sh, make-stickers.py
├── tests/qemu-test.sh, tests/checks.sh
├── docs/FINDINGS.md
└── README.md
```

Use shellcheck-clean bash. Every script supports `--help`.

---

## 10. Testing (must pass before calling it done)

**QEMU (automated):** `tests/qemu-test.sh`
- **Setup:**
  - OVMF UEFI firmware, Secure Boot variant if available;
  - **the target disk attached as an emulated SD/eMMC** (`-device sdhci-pci -device sd-card,drive=…`), so it appears as `/dev/mmcblk0` like the real unit;
  - user-mode networking.
- **Steps:**
  1. The install completes unattended and the VM powers off.
  2. Boot from the disk; the console shows the status screen and the onboarding page answers on port 80 (via port forward).
  3. `tests/checks.sh` runs inside the VM (through a debug build) and asserts all of:
     - forwarding sysctls are on;
     - tailscaled is running and has `Restart=always`;
     - the unattended-upgrades config is present and a dry run succeeds;
     - firewall rules are loaded;
     - the watchdog is configured;
     - journald is volatile;
     - no password hashes exist in `/etc/shadow` for any user;
     - there is no sshd in customer builds;
     - disk usage is under 1.5 GB.
  4. Run an optional Tailscale login with a test auth key from an env var. Then verify:
     - the exit node is advertised;
     - traffic from a second test node routes through it, with the firewall on.

**Real Wyse 3040 (manual checklist in README; record results in FINDINGS.md):**
1. **Install:** time it; it should power off on its own.
2. **Onboarding:** run the customer flow with a phone, using both `.local` and the IP.
3. **Exit-node throughput:** iperf3 or a speedtest through the exit node; record Mbps and CPU load.
4. **Power loss:** pull the power 10× under load. Each time, the unit must come back unattended (AC recovery + fsck.repair) and rejoin Tailscale.
5. **Link loss:** unplug Ethernet for 10 min, replug; it recovers without intervention.
6. **Updates:** force an unattended-upgrade with a kernel update; the reboot happens in the window and the unit returns to `Running`.
7. **Lost login:** run `tailscale logout` remotely in a debug build. The onboarding page and console QR return automatically.
8. **Reset stick:** works and writes its DONE file.
9. **Safety interlock:** boot the installer USB on a non-Wyse PC; it refuses and halts.
10. **Watchdog:** in a debug build, trigger a kernel hang with `echo c > /proc/sysrq-trigger`; the unit reboots by itself.

---

## 11. Working method for Claude Code
- Start with `config.env`, the preseed and a skeleton `install.sh`, and get the QEMU install loop green before adding features.
- Then add features in this order: §5.2 → 5.4 → 5.5 → 5.7 → 5.9 → 5.8 → rest.
- Commit after each green step with a descriptive message.
- Keep a running `docs/FINDINGS.md`: every **VERIFY** item, what you found, and what you changed.
- If something in this spec turns out wrong or impossible, stop and tell me, with options. Do not silently work around a **MUST**.
