A pool of hosts where work can start on, or move to, any host that can honestly run it. It has two parts:
- Know what each host can run. Lodgings probes each host once a day and at start, records what it found as tags with their evidence plus a toolchain fingerprint, and tells you when a host's toolchain changed or no longer matches what you expect, before a result from it is trusted or compared.
- Know which hosts should take work now (docs/drain.md). From beats and canary results each
host is
UP,LATE,STALE,LOST,DRAINING,DRAINED,PAUSEDorASLEEP; drain inputs (failed canaries, fallbacks, battery, a pending reboot, a paused marker, your request) say why; a goodbye beat on sleep keeps a closed laptop from reading as lost; and an evacuation offer lists what would requeue and the exact lane-passport commands that would move each parked lane. Advice only: nothing is moved.
Status: early. 0.3.0 is the first public version (the earlier numbers in the changelog were internal and never released) and 0.3.1 the current one; it has run on Linux, WSL, a Mac (Apple Silicon) and native Windows, as a collector and as a probe target, in one test pass on 2026-10-15, and on 2026-10-16 the test workflow passed on hosted Ubuntu, macOS and Windows images and the same commands were compared side by side on WSL, native Windows and macOS, which led to 0.3.1 (see the support matrix for what each run did and did not cover).
It is not a scheduler, a job runner, a deployment tool or a monitor. It does not start, move or stop anything. Live health (memory pressure, load, disk) belongs to a beat from whatever monitors the host; Lodgings adds what a beat cannot say: which OS and toolchain, whether a GPU-backed Chrome exists, whether signing tools are there.
- Honest about gaps. A capability is probed, declared, verified, contradicted or unverified; unknown is never no and never green. A host that cannot be reached keeps its last good record, marked with its age.
- Nothing installed on the hosts for probing. Linux, WSL and macOS get one Python file on standard input; Windows gets
PowerShell on standard input. No agent, no service, no file written there. (Beats, which the
LATE,STALE,LOSTandDRAININGstates need, are sent by a Lodgings checkout on each host; see step 4 of the quickstart.) - Read-only, local, tiny. Python 3.8+ standard library only, no build step, no telemetry, no root. State is a handful of JSON files in one directory. Deterministic: no model call anywhere.
- Drift you can see. The fingerprint changes when a tool version, the OS, the hardware or a browser build changes; the change is recorded and shown for a day.
- Fixtures, not mocks. Any host can be replayed from recorded facts, so every state can be shown and tested without a second machine.
Clone this repository (or download a source archive of it) and work from its top folder. You need Python 3.8 or newer.
Nothing to install: the command is python3 bin/lodgings on macOS and Linux and bin\lodgings.cmd on Windows, and the
blocks below are written in the Linux and macOS form. (pip install . in the checkout or the unzipped release also puts a
lodgings command on your PATH. On Debian and Ubuntu, python3 -m venv needs the python3-venv package and pip
needs python3-pip, which a minimal install may lack (or use uv venv); and pip install . leaves a build/ folder
and src/lodgings.egg-info/ in the checkout, which git ignores and you can delete.)
To remove it, run pip uninstall lodgings if you installed it with pip, or delete the checkout or the unzipped release. What it
recorded is only the state directory (the state_dir in hosts.toml; by default ~/.local/state/lodgings on Linux,
~/Library/Application Support/lodgings on macOS, %LOCALAPPDATA%\lodgings on Windows) and your hosts.toml; delete those if you
want them gone. Nothing was installed on the hosts you probed.
-
See it with synthetic data (three invented hosts replayed from recorded facts):
python3 bin/lodgings probe --hosts examples/demo-hosts.toml --state demo-state python3 bin/lodgings status -v --hosts examples/demo-hosts.toml --state demo-stateHOST VIEW STATE PROBED FINGERPRINT TAGS NOTES mac darwin ok 0s 51515edd0bec 22 Mac (fixture) toolchain differs from what is expected expected spacetime 2.8.2 (want 2.10) [warn] os macOS 15.1 · arm64 gpu M3 chrome 150 · webgpu yes ... -
Ask who can run something:
python3 bin/lodgings match --requires gpu,chrome-webgpu --hosts examples/demo-hosts.toml --state demo-state python3 bin/lodgings match --requires macos-signing --same-toolchain-as mac --hosts examples/demo-hosts.toml --state demo-state -
See a toolchain change. Replay the same Mac after an update and read the history:
python3 bin/lodgings probe mac --replay examples/fixtures/macos-arm64-after-update.json --hosts examples/demo-hosts.toml --state demo-state python3 bin/lodgings history mac --hosts examples/demo-hosts.toml --state demo-state -
Probe your own hosts. Copy
examples/hosts.example.tomltohosts.toml(it passeshosts checkas it is), then replace its sample hosts with your machines: each is one oflocal = true,ssh = "alias"(an alias from your~/.ssh/config) orfixture = "file"(a path relative tohosts.toml). Thesshaliases in the sample do not exist on your machine until you change them, soprobereports those hosts as failed. Then:python3 bin/lodgings hosts check python3 bin/lodgings probe # all hosts; add --deep to measure WebGPU on hosts with Chrome python3 bin/lodgings status python3 bin/lodgings watch # probes now and every 24 h, in the foreground (Ctrl-C stops it)To keep
watchrunning across logins, start it from your own supervisor (systemd user unit, launchd, Task Scheduler). Lodgings never installs one.probeandwatchgive facts, not live states.UP,LATE,STALE,LOSTandDRAININGcome from beats (step 5), and a beat is sent by a Lodgings checkout and ahosts.tomlon the host that sends it. With the defaults, a host isSTALEafter 60 s without a beat, so a scheduled job that beats once a minute makes every host readSTALE. For that case set, in the[drain]table of the collector'shosts.toml,beat_interval_s = 60,late_after_beats = 2,stale_after_s = 180andlost_after_s = 900(hosts checkvalidates them), or beat every 15 s with--every.matchanswers capability only: combine it withstatesto see whether a matching host is up. -
See host states, a drain and an evacuation offer (a synthetic hour: a tower with a reboot due in 20 minutes and a lane parked on its WSL view, then a Mac whose lid closes):
python3 bin/lodgings record --replay examples/drain/demo-records.jsonl --hosts examples/demo-hosts.toml --state demo-state python3 bin/lodgings states -v --now 1800003600 --hosts examples/demo-hosts.toml --state demo-state python3 bin/lodgings evacuate host-a-wsl --now 1800003600 --hosts examples/demo-hosts.toml --state demo-state python3 bin/lodgings states --now 1800005400 --hosts examples/demo-hosts.toml --state demo-state # asleep, not lostHOST STATE WORK BEAT SINCE WHY mac UP yes 9s - - host-a-win DRAINED no 9s - a reboot is due in 20 min (maintenance window) host-a-wsl DRAINING no 7s - a reboot is due in 20 min (maintenance window, seen by host-a-win); 1 job running; 1 lane parkedTwo words from that output: a lane is one unit of work (a branch, its worktree and its review history) that is parked on a host, and a passport is the sealed directory
core/passport/bin/lodgings-passportwrites so a person can move a lane to another host (core/passport). A canary is a small recurring test job whose pass or fail a host reports; repeated failures are one reason a host is drained (docs/drain.md).On your own hosts, each one beats (
python3 bin/lodgings beat --every 15s -- ssh collector python3 lodgings/bin/lodgings record -, or--recordon the collector itself) and says goodbye before sleeping (examples/goodbye).lodgings-passportis not installed with Lodgings: an evacuation offer namescore/passport/bin/lodgings-passportof a checkout, and for a Windows host the command ispy -3 <checkout>\core\passport\bin\lodgings-passport. -
Show it on a dashboard.
lodgings chipsprints JSON (lodgings.chips.list1.0): display-ready chips and alerts for each host (see docs/capabilities.md). A dashboard or script reads that, and the core knows no dashboard.
In this table lodgings stands for python3 bin/lodgings (bin\lodgings.cmd on Windows).
| Command | Does |
|---|---|
lodgings hosts check [--strict] |
validate hosts.toml, list every problem |
lodgings probe [HOST...] [--deep] [--save-raw] [--replay FACTS] |
probe now and store the result; --replay uses recorded facts for one host |
lodgings watch [--every 24h] [--once] |
probe now, then on an interval, in the foreground |
lodgings status [-v] |
one line per host: state, age, fingerprint, drift, alerts |
lodgings show HOST |
everything known, with the evidence behind each tag |
lodgings history HOST |
recorded fingerprint changes |
lodgings match --requires TAG,... [--same-toolchain-as HOST] |
which hosts honestly qualify, and why not |
lodgings compare A B |
fingerprint differences between two hosts |
lodgings chips [HOST...] |
display-ready chips and alerts as JSON (what a dashboard or script reads) |
lodgings beat [--goodbye REASON] [--every 15s] [--record | --out FILE | -- CMD...] |
on a host: its liveness and drain inputs (power, pending reboot, paused) as one lodgings.beat/1 record |
lodgings record [FILE...] [--replay] |
on the collector: take beats and events (lodgings.beat/1, lodgings.event/1) |
lodgings states [HOST...] [-v] [--now EPOCH] |
each host's state, whether it takes work, and why |
lodgings drain HOST [--reason TEXT] / lodgings undrain HOST |
record or withdraw an operator's drain request (advice only) |
lodgings evacuate HOST |
what would requeue and the exact passport commands for each parked lane; runs nothing |
lodgings ledger [--days N] [--dry-run] |
read the last closed UTC day(s) into the pool's value ledger, counted from its own records; a held day is only superseded by a better reading. Run it at least every hour, e.g. 17 * * * * lodgings ledger --hosts <hosts.toml> --state <state-dir> (docs/ledger.md) |
lodgings thresholds [--from FILE] |
the drain thresholds in force and what a recorded history would have done under them |
--json gives machine-readable output on every command that prints a table. Plain text first, no colour. Exit codes:
0 ok, 1 findings (check errors, no eligible host, hosts differ, no probe yet), 2 usage or configuration error,
3 a probe failed, 70 internal error.
Start, stop or install anything on a host; run a command outside the closed list in docs/safety.md; read a credential or a user's files; write outside its state directory; send data anywhere you did not configure; act on a finding; move a job or a lane (a drain and an evacuation are advice and offers).
Every word and number Lodgings prints comes from a probe, a beat or a record it holds, and a value it does not have reads as unknown, never as zero or as a guess. This section says what that means here.
- Measured. Tool versions, OS, hardware and Chrome facts come from the probe on that host (docs/safety.md, docs/capabilities.md); power, pending reboot and paused marker come from a beat the host sent (docs/drain.md). Each is as old as its probe or beat, and the age is shown.
- Derived. The fingerprint and drift (docs/capabilities.md), and each host's state (
UP,LATE,STALE,LOST,DRAINING, ...) and its reason, are computed from those records and the thresholds in docs/drain.md. An evacuation offer is derived from recorded jobs and lanes. - Unknown. A host with no beat is
UNKNOWN, never green; a tag the probe could not check is unverified, never no; a reboot or battery value the host cannot report is null or unknown (docs/drain.md, docs/capabilities.md). - Stale and silent are shown as such. A host that cannot be reached keeps its last good record, marked with its age
(
stale,failed,never), and silence readsLATE,STALEorLOST, not healthy. A host that said goodbye readsASLEEP, and this machine's own sleep never counts against a host (docs/drain.md). - What it cannot know. Whether a probe or a beat is right, why a host went quiet (a sleeping laptop and a dead network can look alike until a goodbye or a later beat), or anything between two probes or beats. A pending reboot's time is unknown on macOS and Windows unless you pass it.
- What it never claims. That a host is fine because nothing was heard; that an unchecked capability is absent or present; that a drain, a requeue or an evacuation happened. It makes no model call (see "Read-only, local, tiny" above).
- Advice only. Drain requests, host states and evacuation offers move and stop nothing; the commands an offer prints are
for a person to run (docs/safety.md, "What
lodgings evacuateprints" and "What it will never do").
- docs/hosts-toml.md: every key of the registry.
- docs/capabilities.md: raw facts, the tag catalogue, the fingerprint, drift, expectations.
- docs/host-tags.md: what host tags are, how a probe produces them, the fingerprint and drift, and how to use tags in a hosts file to place work, with real commands and output.
- docs/drain.md: host states, drain inputs and their thresholds, goodbye beats, evacuation offers, sleep-safe time, the beat and event records.
- docs/ledger.md: the pool's value ledger: what is counted, what is left unmeasured and why, the digests.
- docs/safety.md: the exact commands a probe and a beat may run and what is written.
- docs/windows.md: PowerShell over ssh, nothing installed; what it cannot see; what differs when Lodgings itself runs on Windows.
- docs/troubleshooting.md: real failure messages.
- Integrations read the JSON seams:
lodgings chips(lodgings.chips.list1.0),lodgings states --jsonand the passport format (core/passport/docs/format.md). A connector for your own tooling is written against those seams (examples/drain/demo-records.jsonlshows the records one sends); the core knows none. examples/: a small pool, three recorded fixtures, a demo registry, a synthetic hour of beats and events, and goodbye-beat hooks for each OS (examples, never installed).CHANGELOG.md,CONTRIBUTING.md,SECURITY.md,CODE_OF_CONDUCT.md,SUPPORT.md, and issue and pull request templates under.github/. Help and reporting: SUPPORT.md, SECURITY.md, CONTRIBUTING.md.
| Probe as a target | Lodgings itself (collector) | |
|---|---|---|
| Linux / WSL2 (Ubuntu) | run, live | install, quickstart, demo and the whole repo suite (47 of 47) on Python 3.12.3 and 3.14.4; on 2026-10-16 the suite (47 of 47), the passport smoke and end-to-end checks (112 of 112) and the side-by-side comparison below again on 3.12.3; Python 3.8 to 3.11 not run here |
| Windows 11 (PowerShell 5.1 over ssh) | run, live: probe 2.9 to 3.1 s | run on Python 3.12.4 and 3.12.10 through bin\lodgings.cmd and pip install .: install, quickstart, demo and probe pass. On 2026-10-16, on 3.12.4: the repo suite (47 tests, 3 skipped by design, 0 failed), the passport smoke and end-to-end checks (112 of 112), the passport demo loop (export, verify, import, confirm, and the squashed-gate case with --adopt-gate-head) and the side-by-side comparison below. The comparison found two defects, fixed in 0.3.1: ledger digests differed from Linux and macOS, and a probe of an unreachable ssh alias hung. The hosted Windows job of 2026-10-16 ran the suite and passed (below) |
| macOS 27 (Apple Silicon) | run, live: probe, one-shot beat (pmset) and --record |
run on Python 3.14.6: install, quickstart, demo, probe of itself, beat, the whole repo suite (47 of 47), the passport demo loop, smoke and end-to-end checks (112 of 112 on 2026-10-16), and the comparison below |
Side-by-side comparison, 2026-10-16. The quickstart, hosts check, probe, status -v, show, match, compare, a
replayed update and history, the drain demo (record, states, evacuate, thresholds, ledger, drain and undrain),
a probe and a one-shot beat of the machine itself, an unreachable ssh alias, a hosts file in a folder with a space and a
non-ASCII label, and the passport demo loop were run from one checkout archive on Windows 11 (3.12.4), WSL2 (3.12.3) and
macOS (3.14.6), and the exit code and output of each were compared after replacing paths, times and commit-derived hashes.
After the fixes the exit codes are identical and the output is identical except where it is inherent to the platform:
the machine's own facts, the resolver's wording of an unresolvable host, the CRLF line ends and code-page bytes of a
Windows pipe, and timing (docs/windows.md). Not covered: a Windows
collector probing a real ssh host (the machine used had no key to reach one).
Hosted run, 2026-10-16. The test workflow (.github/workflows/test.yml), started by hand,
passed on all three hosted images, ubuntu-latest, macos-latest and windows-latest. Each job checks out the
repository, sets up Python 3.12 and runs python -m unittest discover -s tests -v; nothing else. Hosted images are
GitHub's, not a developer's own machines, and they are not the hosts in the rows above.
Not run on real hardware: the goodbye beat on sleep (replayed from fixtures only), and --deep (it starts a headless
Chrome). The sleep and wake behaviour, and the claim that a closed laptop reads ASLEEP and not LOST, are therefore
unproven outside the replay.
What is open today. The support matrix above is the record of what has run; this list only names the gaps in it and the limits of what is measured.
- Python 3.8 to 3.11 is supported on paper (
requires-python = ">=3.8") but the test suite has not been run on it; the runs, hosted ones included, are on 3.12 and 3.14, and the passport's 3.8 to 3.10 support is a syntax check only (passport support matrix). - Beyond the hosted
ubuntu-latest,macos-latestandwindows-latestimages (the unit tests only, on Python 3.12), the runs are on Ubuntu (Linux and WSL2), Windows 11 and macOS 27; other distributions and versions have not been run. The test workflow starts by hand only, so nothing runs it on a push or a pull request. - The goodbye beat on sleep has only been replayed from fixtures, so a closed laptop reading
ASLEEPand notLOSTis unproven on real hardware. --deep(which starts a headless Chrome to measure WebGPU) has not been run on real hardware, and its cost is not measured (resource budget).- A Windows probe goes over ssh, so it cannot see whether Chrome gets the GPU in the desktop session;
chrome-webgpustays unverified there (docs/windows.md). - A pending reboot is not measured on macOS, and its due time is unknown on Windows unless you pass it (docs/drain.md).
- A Windows collector has not probed a real ssh host: the machine used had no key to reach one. Its ssh path is covered by
a stand-in
sshin the suite and by the realssh.exefailing to resolve an alias. OpenSSH for Windows'ssh.exestalls when its standard error is a pipe (found 2026-10-16, worked around by reading it through a file), so a differentssh.exebuild could behave differently. - Timings were measured on one WSL2 machine; an unloaded Mac and Windows are not measured (resource budget).
A probe: Linux about 0.5 s, Windows about 3 s over ssh (2.9 to 3.1 s in 5 runs on 2026-10-15), deep adds up to 1 s for one headless Chrome (not measured in the latest pass); once a day. Lodgings holds nothing in memory between probes and has no idle cost.
A beat: about 0.06 s and 23 MB once per interval (--every 15s keeps one Python process of that size sleeping between
beats). lodgings states over a thousand recorded events: about 0.05 s. Stored records are capped per host (500 beats,
5000 events, 200 transitions).
Budgets, and backing off. Each run has a wall-time budget and each loop a CPU budget, and the loops lengthen their own interval, and say so, when the machine cannot spare them:
| Budget | Measured here | |
|---|---|---|
| one beat, measuring | 2 s wall (the only commands it runs, pmset and sysctl on macOS, time out at 2 s; a reading that takes longer is an unknown, not a zero) |
0.056 s median, 0.059 s at most (30 one-shot runs) |
| one probe of this machine | the per-command timeout_s (default 20 s); over ssh a host gets 4 x timeout_s + 30 s in all |
0.24 s median over 15 runs; the first run after a quiet spell took 2.3 s |
a beat loop (--every) |
10 CPU-seconds per rolling hour, its delivery command included | 0.0008 s of CPU per beat once the interpreter is running: 0.2 s per hour at the default 15 s interval |
| a beat on a schedule instead (a new process every time) | none: it cannot pace itself | 0.056 s of CPU per run, so 13 s per hour at 15 s: use --every |
Back off when the 5-minute load average per core is above 1.0, or the machine is on battery below 40% (or on battery with
the charge unknown), or the loop went over its CPU budget: the interval grows 2, 4 or 8 times (4 times on battery, twice
over budget), never below 5 s or above 120 s, and returns to normal once the load is under 0.75 per core. A paced beat
carries paced: {reason, factor} and the interval it now keeps, so lodgings states reads the host as up but slowed on
purpose, never late and never silent (docs/drain.md, "Back-off and paced beats"). lodgings watch waits
for a quiet machine in five-minute steps, an hour at most, before it probes. --no-pace turns all of it off.
Measured on one machine: WSL2 on Windows 11, 24 logical cores, Python 3.14, at a load of about 0.1 per core. Method: 30
one-shot lodgings beat runs and 15 lodgings probe runs of the local host, each timed from outside with
time.perf_counter and CPU read from getrusage of the finished children (user plus system); the loop's CPU is the
change in utime + stime of the one running process over 60 s at --every 5s, divided by the beats in that time and
scaled to 240 beats an hour. Scripts and raw numbers are not shipped. Other platforms are not measured cleanly here:
the table was not measured on macOS or Windows. A Mac was timed once, at a load average of 180 to 250 on a shared machine,
so its figures are inflated and only show the order of magnitude (a probe 1.4 to 2.6 s, a one-shot beat 1.25 s wall);
an unloaded Mac is not measured. Windows has no load average, so a Windows loop can back off only on battery and CPU
budget.
bin/lodgings(.cmd) the command
src/lodgings/ registry, probes (probe_unix.py, probe_windows.ps1), facts, chips, store, CLI;
beats and events (records.py, beat.py), host states (avail.py), evacuation offers (evacuate.py),
threshold evidence (evidence.py), sleep-safe time (clock.py), value ledger (ledger.py)
docs/ examples/ contracts, fixtures
scripts/ self-audit, merged-check, build-package
tests/ behavioural tests through the public command
MIT licensed, see LICENSE.