rowan.id.au— Piers Rowan

HomePiROS › CONTRIBUTING.md

Contributing & multi-machine development

PiROS is built from one repository (piersrowan/pros) that can be worked on from several machines at once — including more than one Claude Code agent. This document is the coordination contract: how work is split so two people (or agents) rarely touch the same file, how it flows back together through git, and how a clean Ubuntu machine gets from git clone to a booting kernel.

Machine roles

run the OS under QEMU or on bare metal. They never cut releases.

place that runs release.sh, bumps the build number, and publishes pros-rN.img. Keys never leave it. (Currently the macOS box.)

The split below assumes this: dev and test happen anywhere; releasing happens in one place.

Setting up a clean Ubuntu laptop

Ubuntu on Intel is the ideal test rig — it runs QEMU with KVM acceleration (far faster and more faithful than software emulation) and can boot PiROS on bare metal.

# 1. toolchain — rustup reads rust-toolchain.toml and installs nightly + rust-src +
#    llvm-tools-preview automatically. The freestanding target is built with build-std
#    (see kernel/.cargo/config.toml), so there is NO `rustup target add` step.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. system packages
sudo apt update
sudo apt install -y build-essential git qemu-system-x86

# 3. clone + build
git clone git@github.com:piersrowan/pros.git
cd pros
./build.sh --build        # build only (kernel + signed programs + ramdisk + bootable image)
./build.sh                # build + boot in QEMU

On the first build, build.sh generates a local dev keypair into keys/ (it's gitignored, so a fresh clone has none). That build is internally consistent — its kernel trusts programs signed by its own dev key — and is perfect for dev and test. It is not a real release: a real release must be signed by the authority key, which is exactly why releases happen on one machine.

Fast QEMU (KVM)

On Intel with VT-x, add KVM for near-native speed:

kvm-ok                    # confirm virtualization is available (install cpu-checker if missing)
qemu-system-x86_64 -enable-kvm -cpu host \
  -drive format=raw,file=dist/pros-bios.img,if=ide,index=0 \
  -drive format=raw,file=dist/data.img,if=ide,index=1 \
  -netdev user,id=n0 -device rtl8139,netdev=n0 -serial stdio -m 256M

This is the environment the macOS box can't provide (it only has software emulation). Concurrent-SSH behaviour and upgrade-download throughput are far more representative here.

Bare metal — the real test

PiROS is a bootable x86-64 MBR image. To run it on the laptop itself, write the image to a spare disk or USB stick and boot from it (legacy/BIOS boot):

sudo dd if=dist/pros-bios.img of=/dev/sdX bs=4M status=progress && sync   # /dev/sdX = the USB — NOT a system disk

dd to the wrong device destroys it. Triple-check /dev/sdX with lsblk first.

See docs/port-braswell.md for Intel-hardware specifics (NIC, disk controller, serial). Bare metal is the only place that settles hardware-dependent questions — real NIC ring behaviour, real disk PIO timing, real concurrent clients — the things QEMU has hidden from us.

The git workflow

main is always releasable. Nobody commits to it directly.

  1. Branch per unit of work: git switch -c feat/tcp-window-scaling.
  2. Small commits; ./build.sh --build must pass before you push.
  3. git push origin feat/tcp-window-scaling.
  4. Open a PR (gh pr create) or hand the branch to the integrator. **The release machine reviews and

merges to main** — it is the gatekeeper.

  1. Everyone rebases on main often: git fetch && git rebase origin/main.

Small, frequent PRs win: a 200-line PR merges clean; a 2000-line one fights everyone else's work.

Two files never conflict because they're gitignored and regenerated per machine: keys/ and kernel/src/generated/version.rs (the build-number counter). Don't commit either.

Who owns what — the coupling map

Merge pain comes from two people editing the same file, or one changing an interface the other calls. Split by the first column; the third is where you'd collide.

Splits that stay out of each other's way

ComponentFilesNotes
NIC drivers + tuninghal/rtl8139.rs, hal/e1000.rs, hal/nic.rsself-contained; one NIC per person. NIC tuning = Dell
Disk drivershal/ata.rs, hal/ahci.rs, hal/disk.rsindependent of the NIC
TCP + congestion controlnet/tcp.rs, net/tcp_input.rs, hal/tsc.rssocket pool + state machine, RTT/RTO + Reno, TSC clock. = Dell
TLS / SSHnet/tls.rs, net/ssh.rsSSH builds on TLS. ssh.rs is now a sched.rs chokepoint too (see below)
Memory / pagingmemory.rsframe allocator, page tables, DMA/cache slabs
Storage cache map / VFScache.rs, vfs.rs, ramdisk.rsthe cache map + fd file layer. = Mac
Upgradeupgrade.rsdownload → cache → verify → boot disk
Auth / cryptousers.rs, localca.rs, vault.rs, authz.rs, pros-cert/leaf crypto has a stable API
Userspace programsprograms/*separate binaries; fully isolated from the kernel

Chokepoints — coordinate before touching

One owner at a time. Concurrent SSH now rides this: net/ssh.rs's per-connection worker model depends on spawn_arg/task_arg (hand the accepted socket to the worker) and on the KSTACK_SIZE / MAX_TASKS consts — touching either the sshd worker path or those consts is a sched.rs + ssh.rs cross-file change, so coordinate.

structs (Socket, Task, Current). Edit rarely; announce it in TASKS.md.

Worked example: TCP on the laptop, memory here

is memory reserves slabs and exposes stable accessors (dma_virt(), cache_virt()); TCP only consumes them. As long as those signatures don't change, the two never cross. "TCP needs a bigger buffer" is a small, explicit handoff — not a constant collision.

The rule that prevents silent breakage

Git catches text conflicts. It does not catch semantic ones: if the laptop changes a function signature this machine calls, the merge is clean and then the build fails.

Freeze the interface between two parallel components for the duration of the split. A change to a shared signature (an accessor, a struct field, a pub fn both sides use) is a stop-and-sync event, not a solo edit.

Agree the boundary up front, record it in TASKS.md, and treat it as a contract.

Coordinating multiple agents

Two Claude Code agents don't share context or memory — only the repo. So the repo is the shared brain:

Don't assume one agent knows what the other is doing except through these.

Cutting a release (release machine only)

REMOTE_USER=<you> ./release.sh    # build + sign + publish, repoint pros-current.img

Only on the machine with the authority keys/. It bumps the build number, uploads pros-rN.img, and updates the public index. Never run two releases from two machines.