Upgrade a 2007 server to today, one release at a time, along a path someone already proved works.
wend is for people running fleets of old servers on Linux releases that stopped getting updates years ago. A distro's own upgrader moves the OS; wend moves the server: every hop carries the box's services and data along (web server, PHP, database, DNS) and checks they still answer before the next one. Debian etch to trixie, Ubuntu trusty to resolute and CentOS 5, 7 and 8 to AlmaLinux 10 have each passed on real installs in a VM, with a real reboot after every hop.
This repo is the map: small, reviewed fixes for the places where a plain release upgrade breaks, and routes that
pin those fixes into release-to-release paths. Each route carries evidence (runs) that it was replayed from scratch on
real test boxes and every health check stayed green. A client (a static Rust binary, being built in client/)
fingerprints your machine, picks a route, and walks it. At each hop it applies the fixes whose preconditions hold on
your box, takes a snapshot first, and rolls back if a check fails. The client uses no model. A walk pinned to a
proven run (--run) fetches the exact bytes that run used, by sha256, from your own cache, wend's public package
archive (https://archive.fieldmouseworks.com, docs/archive.md) or your distro's mirrors, and
refuses anything that doesn't match. A --live walk takes today's mirrors unverified, and its run says so.
Five routes so far. Every box below passes its route's current version from scratch, with no model:
| Route | Fixes | Boxes | Runs |
|---|---|---|---|
| Debian 4.0 etch (2007) to Debian 13 trixie | 14 | etch-web (PHP web server), etch-db (MySQL/BIND) |
CI |
| CentOS 5.11 (2014) to AlmaLinux 10 | 36 | centos5-web (web/MySQL), centos5-dns (DNS/FTP) |
CI for centos5-web, local for centos5-dns |
| CentOS 7.9 to AlmaLinux 10 | 24 | centos7-lamp, centos7-pyapp (a Python 2.7 app on MariaDB) |
local |
| CentOS 8.5 to AlmaLinux 10 | 20 | centos8-lamp |
local |
| Ubuntu 14.04 trusty to 26.04 resolute | 8 | trusty-web, trusty-web-updated, trusty-db |
local |
CI runs are attested by the gate. Local runs were filed before that rule (CONTRIBUTING.md, "How runs land"), and CI replays of the same routes will replace them. The Debian route began as the dep-lab results. The CentOS 5 route crosses yum to dnf, the /usr merge, two package-manager bootstraps and a vendor switch; Opus 5.5 found the path in all 5 of its dep-lab runs, and replays are free.
CI replays a route in a chroot whenever a change touches it, which proves the userland only. Kernels, bootloaders and
the first real boot are tested by the VM tier (tools/vm.py, SPEC.md "VM tier"), which the maintainer runs: each box
is installed by its release's own installer in a KVM guest, and every hop ends with a real reboot and the box's
checks against the live services. As of 2026-10-14, at the route versions recorded there, these boxes have passed it
from scratch on BIOS: etch-web, etch-db, trusty-db, trusty-web-updated, centos5-web, centos7-lamp and
centos8-lamp. All three Ubuntu boxes have also passed on UEFI. The results are on
wend#13, #84
(UEFI) and #92 (CentOS 8).
Agents and contributors: start with AGENTS.md. Clone, plug in any model, discover a path, prove it with the same command CI runs, and open a PR.
Early days. The fix format (
wend.fix/0) now covers two package managers: ops are typed, and each family's adapter names a backend (apt or rpm) that compiles them. Open questions remain (see SPEC.md); fixes written now will be migrated by a script, not abandoned.
The map grows from people using it:
- Use. Someone runs the client on their old box. It walks a route and applies the fixes that match.
- Gap. A hop breaks. The client rolls the box back, so nothing is lost, and stops.
- Report.
wend report <run file>builds a gap report from the failed run: which hop, which check, the log tail. It scrubs hostnames, IPs, emails, local users and anything that looks like a secret, shows the whole thing, and prints a prefilled GitHub issue link. Nothing is sent until the user opens that link and clicks submit. No model, no token, no account linking. - Triage. The
gapsworkflow merges repeat reports: the same gap key becomes a +1 on one issue. A maintainer labels the gaptriaged, and a bot opens a PR adding it towanted.toml. - Discover. An agent claims it (AGENTS.md), builds or picks a box like the reporter's, runs
wend discover, and opens a PR that saysCloses #N. - Merge. CI replays the routes it touches, remi proves it on real installs when a chroot can't judge it
(CONTRIBUTING.md,
remi-proof), the Claude review signs off, and the maintainer or Claude merges. The issue closes, and the reporter is pinged to retry.
Every gap that gets closed makes the map work on one more kind of machine.
SPEC.md the format: fix, route, run, box, adapters, tiers, open questions
AGENTS.md the playbook: claim, box, model, discover, self-review, verify, PR
CLAUDE.md points at AGENTS.md
CONTRIBUTING.md the five-layer PR gate, for humans and agents
wend the one command: check, list, replay, discover, report, gaps, verify
wanted.toml paths people want; claim them via their issue
prompts/discover.md the exact question a model gets when a hop breaks
NAMES.md why "wend"
LICENSE-MIT, LICENSE-APACHE dual license
adapters/
debian.toml, ubuntu.toml apt families: ladder, mirrors, how a hop runs, predicates, writable allowlist, ops, checks
el.toml Enterprise Linux: CentOS 5, 7 and 8 to AlmaLinux 10, the rpm backend, the mirror shim
drivers/<family>/ each release's driver table (SPEC "Hardware preflight")
leapp/el/ AlmaLinux's leapp-data, vendored (SPEC "Package evolution data")
fixes/<family>/<hop>/<name>.toml 14 debian, 46 el, 8 ubuntu
routes/<family>/<from>-to-<to>.toml the five routes above
boxes/<family>/<name>/box.toml CI fixtures, trusted test code, never shipped
runs/<family>/<route>/<box>/<stamp>.json new runs land from CI only: the gate refuses a run it didn't attest
archive/allowed_signers the archive's public signing key (docs/archive.md)
tools/
check.py schema check, content ids, decoy check, --repin, --compile
backends.py what each typed op compiles to: apt (Debian, Ubuntu) and rpm (EL)
mirror.py http -> https cache for old boxes (CentOS 5's yum can't do today's TLS; trusty's apt lacks https)
sandbox.py chroot boxes: build, snapshot, roll back, private /dev
walk.py walk a route on a box (the map walk), write the run
vm.py the VM tier: replay a route on a real install in a KVM guest, a real reboot after every hop
discover.py a hop broke: ask the model, check the answer like a PR, try it
report.py a hop broke and you won't fix it: scrubbed gap report + prefilled issue link
models.py bring your own model: anthropic, any OpenAI-compatible server, or the agent itself
affected.py which routes and boxes a change must replay (CI matrix, ./wend verify)
file_runs.py files CI's run artifacts
pesgaps.py, leappdata.py what leapp-data says changes at each EL hop, and its vendoring
hwdata.py, runtimes.py build the driver and runtime tables for hardware and runtime preflight
shelter.py, podman_standin.sh sheltering an app (SPEC "Sheltering an app"), chroot tier
net_*.py reference implementations of the network ops
archive.py, fetched.py, gate_fetched.py, r2push.py the archive: capture, store, publish
tomlw.py writes TOML (tomllib only reads)
port_oldlinux.py one-off import of the dep-lab Debian results, with a lossless round-trip proof
test_*.py the tools' own tests; test_check.py holds the broken fixes the gate must refuse
docs/validation/ the first end-to-end test: verify on both boxes, and Opus refilling a deleted fix
docs/try-it.md for a machine's owner: get the client, run `wend share` (read-only), post the report
docs/archive.md design: the wend archive (wend#19), every byte proven routes reference, by sha256
docs/rebuild-and-carry.md design: moving a machine whose filesystem a later kernel can't mount (old-XFS CentOS 7)
client/ the Rust client (client/README.md): reads, checks and compiles the atlas exactly as tools/
does (client/parity.py proves it), walks a route on a root filesystem, and runs hardware and
runtime preflight. Fetching by digest is the current slice. It doesn't walk the running
system yet: that needs a snapshot backend. No AI.
.github/
workflows/gate.yml the PR gate: dco, schema, plan, replay, decoys, claude-review
workflows/client.yml the Rust client: build, test, parity with tools/
workflows/gaps.yml the gap loop: dedupe reports, triaged gap -> wanted.toml PR, ping the reporter on close
workflows/release.yml on a version tag: the static client and the atlas it reads, as a release download
workflows/remi-proof.yml the remi-proof status: holds PRs a chroot can't judge until remi passes their head
pull_request_template.md, ISSUE_TEMPLATE/route-gap.yml, ISSUE_TEMPLATE/box-report.yml,
ISSUE_TEMPLATE/wanted.md, dependabot.yml
CODEOWNERS a human on everything; the owner on tools/, adapters/, SPEC.md, .github/
Got an old CentOS, Debian or Ubuntu machine? sudo ./wend share, from the
latest release, reads it without changing anything, says
whether its release's route would get it to the end, and writes a report with the hostname, users and addresses
scrubbed, plus a prefilled issue link. docs/try-it.md says what it reads and what it scrubs.
python3 wend check # schema check plus the gate's own tests, no root needed
python3 wend list
sudo python3 wend replay routes/debian/etch-to-trixie.toml debian/etch-web # ~15 min, needs root + debootstrap
Standard library only, Python 3.11+.
Everything here, code and content alike (fixes, routes, boxes, runs), is
dual-licensed under MIT OR Apache-2.0, at your option. That's the Rust ecosystem norm, and Apache adds an
explicit patent grant. See LICENSE-MIT and LICENSE-APACHE. Contributions are accepted under the same terms, with
a DCO sign-off (see CONTRIBUTING.md).