cc-libs/docs/adrs/adr-0005-craftos-pc-harness-and-probes.md

8.5 KiB

ADR 0005: CraftOS-PC Harness, Periphemu Bootstrap, and Headless Probes

Status

Accepted

Date

2026-06-08

Context

This repository targets CC:Tweaked on Minecraft 1.21. The Lua we ship runs inside the ComputerCraft sandbox: it depends on os.pullEventRaw, peripheral, rednet, textutils.serializeJSON, modem channels, and CC-specific globals. Standard Lua cannot execute this code as-is, so a normal local test harness was never a serious option.

Contributors have been running the code in two places:

  • In-game on a real Minecraft server, which is slow to iterate on.
  • In CraftOS-PC (https://www.craftos-pc.cc/), a desktop emulator that ships the same ROM/BIOS as CC:Tweaked, supports modem peripherals via periphemu, and can run fully headless (--cli --headless --script <file>).

CraftOS-PC was the de facto local harness for months but lived only as a single line in CLAUDE.md. There was no install guide, no minimum version, and just install did not check the binary was present. Two related concerns emerged on top of that:

  • startup/servers.lua historically called periphemu.create four times on computer 0 (a top modem, two computer peers at ids 1 and 2 both labelled Trap, and a router peer at id 10). In CraftOS-PC GUI mode this opened four windows on every launch, and the duplicate Trap label plus persistent per-id state across versions caused recurring confusion.
  • Headless CraftOS-PC is also a cheap, deterministic interactive tool: it boots the emulator, runs an arbitrary Lua snippet against the real CC:Tweaked ROM, prints output, and exits in well under a second. Humans and agents can use it to verify hypotheses about CC:Tweaked behavior before writing code or tests. That usage was implicit; no document framed headless exec recipes as the recommended first move when an agent is unsure about CC:Tweaked behavior.

Decision

1. CraftOS-PC as a first-class local dev dependency

  • Minimum version v2.8.3 — recent enough to have the current CC:Tweaked ROM, old enough that contributors already on a 2.8.x build will not be forced to upgrade again.
  • Documented install in docs/install-craftos-pc.md, with a SHA-256-verified macOS flow and pointers to the official Windows/Linux artifacts.
  • Documented upstream navigation in docs/craftos_pc_glossary.md, covering CLI flags, mounts, periphemu, save data, and troubleshooting pages.
  • Verified by just install via check-install, which checks craftos, jq, luacheck, and openssl. check-craftos runs craftos --version and requires v2.8.3 or newer. Failure prints a one-line pointer to the install guide.
  • Repository-local TrapOS launch. just trapos runs CraftOS-PC with --directory .craftos, keeps the macOS --rom /Applications/CraftOS-PC.app/Contents/Resources workaround, mounts the repository root read-only at /trapos, and mounts each top-level source directory read-only at its ComputerCraft root path.
  • Vanilla launch. just craftos launches CraftOS-PC under .craftos-vanilla/ with no mounts and no startup, for probes that should not see TrapOS files and for the just trapos-install end-to-end install verification.
  • just ci is the local verification entry point. It runs check-craftos, check, and test. Local Git hooks are installed by just install; see ADR-0011 for the commit/push split.

The existing CLAUDE.md constraint ("Do not run Lua locally or add a test harness unless asked") is reframed rather than removed: there is still no standalone Lua harness, and we are not adding a Busted-style test runner. The harness is CraftOS-PC, invoked deliberately.

2. Periphemu bootstrap stays minimal

startup/servers.lua attaches only a top modem under periphemu:

if periphemu then
  periphemu.create('top', 'modem');
end

Extra emulated computers are spawned manually from the CraftOS-PC shell when actually needed (e.g. periphemu create 10 computer to bring up a router peer for cross-VM testing). The pattern is documented in docs/periphemu.md. The if periphemu then guard is preserved so in-game behavior is unchanged.

3. Headless probes as the canonical hypothesis pattern

Two safe-exec recipes wrap raw --headless --exec with xpcall, call os.shutdown() on success or Lua error, and use TRAP_CCLIBS_HEADLESS_TIMEOUT_SECONDS (default 10) as a host watchdog:

  • just trapos-exec '<lua>' — probe against the TrapOS dev environment. Mounts of /apis, /programs, /servers, /startup, /tests, and the repo root at /trapos are live, so require('/apis/eventloop') and friends work against the current branch. Use this when the question involves repo code.

  • just craftos-exec '<lua>' — probe against vanilla CraftOS-PC. No mounts, no startup scripts. Use this when the question is purely about CC:Tweaked behavior and TrapOS files would be a distraction, or to confirm a behavior is upstream rather than something the dev env layered on.

  • just trapos-install — drive the full real install (install-ccpm.luaccpm updateccpm install trapos) on a fresh ephemeral state. This is the probe to run when changing anything in the install path itself.

Conventions:

  • Prefer the safe exec recipes over raw --headless --exec.
  • Keep snippets minimal and side-effect-free. If a probe reveals a fact worth defending, add a libtest case under tests/ — probes are not a substitute for committed tests.
  • LLM agents SHOULD prefer a quick headless probe over speculation when answering "does X work in CC:Tweaked?" or "does my refactor still load?". The cost is one extra emulator boot (~1s); the benefit is grounded answers instead of plausible-sounding ones.

Consequences

  • Contributors must install CraftOS-PC before just install succeeds. The install guide makes this a 4-step copy/paste on macOS.
  • Headless tests live under tests/ and are driven by just test. See ADR-0007 for the runner and timeout layering.
  • The macOS install symlinks the binary into /usr/local/bin, which makes CraftOS-PC unable to auto-discover the ROM that ships inside the .app bundle (Could not mount ROM). The test: recipe works around this by passing --rom /Applications/CraftOS-PC.app/Contents/Resources on Darwin. Linux (AppImage) and Windows (installer) auto-discover correctly.
  • just trapos uses repository-local save data under .craftos/config/ and .craftos/computer/. This keeps emulator state out of ~/Library/Application Support/CraftOS-PC during repository work and keeps repo files visible through read-only mounts instead of copying them into the VM save.
  • just repl is a human-only interactive wrapper around just trapos --cli; automation and LLM agents must use just trapos-exec '<lua>' or just craftos-exec '<lua>' instead.
  • craftos (GUI) now opens a single unlabelled Computer 0 window with a top modem attached. Cross-machine testing requires an explicit periphemu create call from the shell rather than being implicit on boot — one extra command when you need a peer, no surprise windows or persisted ghost VMs.
  • .craftos-vanilla/ is in .gitignore alongside .craftos/.
  • just trapos-install is not part of just ci: it is network-dependent and slower than just test. Run it manually when touching install-ccpm.lua or ccpm package descriptors.
  • Higher CraftOS-PC invocation traffic during agent sessions; cheap enough that this is a good trade.
  • The harness version becomes a project-level concern. When CC:Tweaked ships breaking changes that require a newer CraftOS-PC build, we bump the minimum version in docs/install-craftos-pc.md and check-craftos keeps contributors honest.
  • No CI integration yet. Running CraftOS-PC headless in GitHub Actions is feasible (the AppImage works on Ubuntu runners) but is out of scope; the contract is local-only for now.

Future Work

  • API-loading smoke test. Extend tests/ with a script that requires /apis/eventloop, /apis/net, /apis/libtest, and the router, asserting the wiring loads without errors.
  • CI. Run just test on push using the Linux AppImage.
  • Pinned ROM. Point CraftOS-PC at a vendored ROM via --rom if we ever need to test against a specific in-game version.
  • If tests/ grows a multi-VM scenario, drive peer creation from the test script itself (each tests/*.lua already owns its setup) rather than re-adding peers to startup/servers.lua.