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
- Dev / build / test machines (any number) — clone the repo, work on feature branches, build, and
run the OS under QEMU or on bare metal. They never cut releases.
- The release machine (exactly one) — holds the authority signing keys in
keys/and is the only
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.
- Branch per unit of work:
git switch -c feat/tcp-window-scaling. - Small commits;
./build.sh --buildmust pass before you push. git push origin feat/tcp-window-scaling.- 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.
- Everyone rebases on
mainoften: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
| Component | Files | Notes |
|---|---|---|
| NIC drivers + tuning | hal/rtl8139.rs, hal/e1000.rs, hal/nic.rs | self-contained; one NIC per person. NIC tuning = Dell |
| Disk drivers | hal/ata.rs, hal/ahci.rs, hal/disk.rs | independent of the NIC |
| TCP + congestion control | net/tcp.rs, net/tcp_input.rs, hal/tsc.rs | socket pool + state machine, RTT/RTO + Reno, TSC clock. = Dell |
| TLS / SSH | net/tls.rs, net/ssh.rs | SSH builds on TLS. ssh.rs is now a sched.rs chokepoint too (see below) |
| Memory / paging | memory.rs | frame allocator, page tables, DMA/cache slabs |
| Storage cache map / VFS | cache.rs, vfs.rs, ramdisk.rs | the cache map + fd file layer. = Mac |
| Upgrade | upgrade.rs | download → cache → verify → boot disk |
| Auth / crypto | users.rs, localca.rs, vault.rs, authz.rs, pros-cert/ | leaf crypto has a stable API |
| Userspace programs | programs/* | separate binaries; fully isolated from the kernel |
Chokepoints — coordinate before touching
sched.rs+syscalls.rs— theTask/CURRENT/ context-switch code is tightly interwoven.
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.
- Spine files —
main.rs(module list),net/mod.rs(re-exports),Cargo.toml, and shared
structs (Socket, Task, Current). Edit rarely; announce it in TASKS.md.
- DMA / cache slab layout in
memory.rs— offsets collide if two people add slabs.
Worked example: TCP on the laptop, memory here
- TCP agent lives in
net/tcp.rs; memory agent inmemory.rs. Different files → no git conflicts. - The one crossover: TCP's buffers are owned by memory (the DMA slab, the cache slab). The contract
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:
CLAUDE.md— house rules both agents read on start.TASKS.md— the live "who owns what right now" board. Update it when you pick up or finish work.- PR descriptions — where the why of a change lives.
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.