PAF becomes saf/device (history kept), STATE.md dissolves into saf/state.md with the dated era archived, the substrate SAF moves up from souveraine, and every agreement points at saf/INDEX.md and nowhere else. one map, nothing to remember
10 KiB
START HERE
If you are picking up the phone and something is broken, this page is the whole answer. Do not go reading doctrine first.
Which phone? There are two, and they are plugged in at the same time. This page is blueline, the Pixel 3 daily driver, at
172.16.42.1. The iPhone 7 is at172.16.43.1, boots with~/Projects/hoolock/boot.sh, and its whole state isdevices/d10/. Never unplug one to make a command convenient.devices/is the per-body support matrix.
Doctrine explains why the system is shaped the way it is and is worth reading before you design something. It is the wrong thing to open when the screen is black. This page is for that.
0. The thirty-second triage
Run this. It answers most of what a session usually spends an hour rediscovering.
Pixel3Arch/tools/phone-triage.sh
It checks, in order: can I reach the phone at all (USB, VPN, wifi), is the compositor up, is the shell up, what does the panel think it is doing, is the session class right, and is the lock/blank ordering holding. Every check prints the fix next to the failure.
1. "The phone is unreachable over USB"
It is almost certainly booted and fine. This exact symptom has cost multiple sessions.
Signature: ip neigh shows the phone REACHABLE (it answers ARP), and ICMP
and ssh both time out.
Cause: a cold boot leaves two gadgets — the initramfs one and the rootfs one
— so the phone ends with a dead usb0 and a live usb1, and both hold
172.16.42.1. Replies egress the dead one and vanish.
Fix, and read this part: IgnoreCarrierLoss=5s in 40-usb0.network has been
correct in the repo since 2026-08-01 and was never on the device, because
rootfs-overlay/ only lands on a flash and this phone updates with pacman.
It is now a package:
sudo pacman -S blueline-usb-gadget # then reboot, or:
sudo ip addr flush dev usb0 # one-shot, on the phone
Until that package is installed on a given device, expect this every cold boot.
Check which gadget you are talking to — this tells you whether the phone finished booting:
sudo dmesg | grep "SerialNumber: " | tail -3
# dbg-initramfs -> still in the initramfs (serial consoles on ttyACM0/1)
# google,blueline -> the real rootfs is up
error -71 on enumeration is a cable, not the phone. device descriptor read/64, error -71 retrying forever is EPROTO — try another port, then another
cable, before touching anything else.
Never warm-reboot blueline to fix a cable. A warm reboot poisons TZ/remoteproc state; recovery from that is a true cold boot. Power off fully.
2. "The screen is black after login"
Regression fixed 2026-08-02 in souveraine-viewtop 1fa1ea1. If you see it
again, the mechanism is worth knowing because the shape recurs:
disclosure_locked() was authority.locked || position.hides_session(). When
the protocol unlocks, position clears — but the authority half stays true
until sessiond's poll catches up, and while it is true frame_elements
composites lock surfaces and nothing else. The client has just destroyed
those surfaces, so the scene renders empty.
The OR was harmless for months because nothing ever set LockedHint. Making
viewtop report it made the half true for the first time and the latent trap
fired. The pattern: a dormant OR becomes a bug the day its other side starts
working.
3. "A control that worked under Hyprland does nothing"
Ask first: was it a hyprland.lua binding? If yes, it did not survive, and
the fix is never to re-add a binding — it is to put the behaviour in the state
machine.
Everything in overlays/hypr/hyprland.lua is a checklist of things that either
have been ported or still need to be. Ported so far: power button, volume,
double-tap-to-wake, DPMS. Still bound to nothing: brightness keys, media keys
(playerctl), osk-switch.
The rule, from DEVICE-STATE-MACHINE.md §12: a small daemon that reads a signal
and calls a tool is the eighth blind actor. Behaviour enters as a
sensor_input/button/input verb and leaves as an Action from tick(),
through the one executor table.
4. Where to build, and where NOT to
| Build host | archdev, ssh -i ~/.ssh/ani root@10.10.20.123, run cargo as casey |
| Never build on | the phone (3.5 G RAM, daily driver) or the laptop |
| viewtop mirror | /home/casey/souveraine-viewtop — not ~/Projects/..., that one is stale |
| The git tree | the laptop. archdev's mirrors are not git repos. |
The loop is edit on the laptop → rsync UP → build → cargo fmt on archdev →
rsync DOWN. Pull down before you push up and you silently revert your own work,
and a build started before the push verifies the old code while looking green.
Verify your change arrived (grep -c <new symbol>) before trusting a result.
viewtop's gates — and cargo clippy --workspace does not cover the render
path, which is the whole kms feature:
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy -p viewtop-compositor --features kms --all-targets -- -D warnings
cargo clippy -p viewtop-compositor --features nested --all-targets -- -D warnings
cargo test --workspace
./scripts/build-cross.sh
smithay's real signatures are on archdev at
~/.cargo/git/checkouts/smithay-312425d48e59d8c8/812bd33. Read them; the laptop
has no checkout.
5. How anything reaches the phone
Three different paths, and picking the wrong one is why things silently never update.
| What | How it ships | Gotcha |
|---|---|---|
souveraine, sessiond, machined, secrets |
pacman, built by CI on push to primary |
Needs the VPN up — the repo is at 10.10.20.120:4455 |
Device packages (pkgs/*) |
pacman, from Pixel3Arch CI | pkgs/** has no workflow for some paths — check |
rootfs-overlay/ |
flash only | This phone does not flash. An overlay-only fix never arrives. This is TASK-28, and it is what broke USB for months. |
| viewtop | pacman, built by CI on push to main |
Packaged since TASK-25 closed. /usr/bin/viewtop is owned by souveraine-viewtop; verify with pacman -Qo /usr/bin/viewtop, whose version carries the commit. Do not hand-copy over it — that is the "silently never updates" trap this table exists for. Installing does not restart it; the running compositor keeps the old binary until the session restarts. |
| QML shell | per-file scp |
Never deploy.sh --phone. Check whether the live path is a symlink into ii/ or a real override; patch every copy. |
A green CI run on primary can ship nothing and look identical to one that
shipped. Read the jobs, not the run status.
6. Reaching the phone
ssh -i ~/.ssh/ani casey@172.16.42.1 # USB tether
ssh -i ~/.ssh/ani casey@10.10.20.234 # home wifi, via VPN
10.10.20/24 is reachable only through the VPN, and it does not autoconnect
after a reboot. If gitea resolves to a Cloudflare address, the tunnel is down.
On the phone: nmcli con up wiufph. On the laptop: wiufarch.
The phone does join networks that are not home. This page used to say it
could not — that away from home the SSIDs are pinned and USB is the only path.
On 2026-08-02 it sat on a foreign SSID at 10.10.30.213 working perfectly while
that paragraph sent the reader hunting for a cable. Pass the address:
Pixel3Arch/tools/phone-triage.sh 10.10.30.213 # or export PHONE_HOST
Find it from the router, or on the phone with ip -br a show wlan0.
A phone on a foreign network cannot reach the package repo, since that lives
at 10.10.20.120:4455 behind the VPN. You do not need to bring wiufph up for
that — the laptop has the tunnel and the phone is on the local subnet, so fetch
on the laptop and push down: gitea release → scp → pacman -U. Leave the
phone's tunnel alone; it is the user's switch.
7. The five traps that have each cost a session or more
- Grepping the design docs. They argue; the argument is the content.
ls -lt docs/and read what is newest. Sessions that grep produce confident, wrong architecture — the single most reliable failure in this project. - Verifying the actuator instead of the invariant. Calling
blueline-screen-toggle offproves DPMS works and nothing about the lock-then-blank ordering, because it bypassesrequest_blank()entirely. Trigger a blank the way the machine does, then read the trail. - The forensic trail is at
~/.local/state/souveraine/forensic.jsonl, not the runtime dir. It moved. Looking in the old place shows an empty file and invites the wrong conclusion. - A greetd restart silently downgrades the session to
Class=greeter, and logind then refusesSetLockedHintoutright — after which every blank goes out on a session nobody could confirm was locked. Fixed by a drop-in, but checkloginctl show-session <seat0 session> -p Classbefore believing anything about locking. - Two actors deciding one thing. It has happened three times: hypridle vs
the idle rule, the compositor vs sessiond over the panel (a press woke the
phone and put it back to sleep), and the shell vs the compositor over
LockedHint. When something oscillates or fights, count the writers first.
8. Then, and only then: the design docs
Read these when you are designing, not when you are debugging.
SESSION-AUTHORITY-DOCTRINE.md— §13 first (it inverts the default the earlier sections imply), then §4, §9, §10.DEVICE-STATE-MACHINE.md— §1 (why one machine, not seven blind actors), §4 (evidence and confidence), §10 (source health), §12 (an Action, not a daemon).LOCK-DPMS-LESSONS.md— §1 is the lock-then-blank invariant and every way it has been broken.docs/tasks/README.md— the live work. 43 (viewtop), 50 (her hand on the glass), 51 (viewtop → daily driver), 52 (atmosphere).
saf/state.md is the resume anchor and goes stale; check its date.