The PC no longer wakes anyone -- the phone does. This deletes the alarm
window, the audio escalation, the sink/reactivation machinery, the power-key
inhibitor, the escape hatch, the curtains, the smart plug, the brightness and
DDC/CI control, the hibernate path (shutdown wrapper + rtcwake + sleep hook)
and the challenge set: 30 modules, 34 test files, ~5k lines.
What stays is the part that still earns its keep: the per-minute trigger
reads the alarm the phone published and starts morning-routine.service, so
the workout lock still runs at the right time.
THE ONE LOAD-BEARING BEHAVIOUR CHANGE. Deleting _alarm.py orphaned the only
writer of wake_state.json, and _trigger gated on it via
was_alarm_dismissed_today(). With that gone -- and was_workout_logged_today()
already permanently False, since WORKOUT_LOG_FILE does not exist -- both
gates would have been dead and the routine restarted on every tick for the
rest of the 60-minute catch-up window. The unit is Type=simple, so a start
while it runs is a no-op, but the moment the workout lock finished the next
tick would relaunch it on top of the user. So _trigger now writes its own
once-per-day latch (save_trigger_fired / was_trigger_fired_today) and gates
on that. Collapsing the catch-up window instead was rejected: that window is
the entire reason a suspended or just-booted machine still fires. It also
replaces the deleted sleep hook, via Persistent=true.
_trigger also imported should_refire from _alarm_session, which was on the
delete list. Since start_session lived only in _alarm.py, no session record
is ever written again and the branch was unreachable, not merely unused.
WAKE_STATE_FILE is now redirected in tests/conftest.py rather than per-file.
The trigger tests never redirected it, because until this commit nothing on
that path wrote anything -- and a per-file redirect is exactly the kind that
gets forgotten when a new writer appears, with a silent failure: a green
suite that quietly stamped the live state file. Verified by mtime that it
did not.
Cross-repo, same change: morning_routine._orchestrator loses its alarm leg
and its --with-alarm flag. Leaving an orchestrator pointing at a deleted
module caused a month-long silent failure once already.
uninstall.sh is new, and matters more than it looks. /usr/local/bin/shutdown
is root-owned and shadows /usr/bin/shutdown; deleting this repo would leave
it in place, and with the alarm cache gone it falls through to rtcwake +
hibernate -- so "shutdown now" would silently stop being a poweroff, every
day, forever. /etc/sudoers.d/wake-alarm is deliberately NOT removed:
screen-locker's own _wake_alarm.py still needs passwordless rtcwake.
159 tests pass at 100% branch coverage; all 26 pre-commit hooks green.
Verified by running it: _sync --show and _trigger --dry-run both resolve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcdngP688mW6rJye3LRkSN
scripts/vmbox/harness.sh provisions a disposable Arch guest, runs the REAL
install.sh in it, and adjudicates 8 cases on a host-side exit code. Plus
DOCS-vmbox.md, which states what a pass does and does not mean.
Covered: system-python imports, timer enabled, wake-alarm.service still
`static` (an [Install] there fires the alarm at *login*), the shutdown wrapper
shadowing /usr/bin/shutdown, the alarm-cache shape jq expects, a trigger tick,
the inhibitor, and the re-fire path. With --window it also opens the real
fullscreen Tk window and screenshots it from the host -- confirmed showing
"WAKE UP!", round 1/4 and a live flash challenge.
Not covered, and said plainly in the doc rather than implied away: audio (the
guest has no pactl/paplay), the phone, GPU and real outputs, and anything
bootloader-related (guest is BIOS, this machine is UEFI).
**The VM earned its place immediately by finding two live bugs that the host
could not reach and the unit suite passed straight through.**
1. The inhibitor claimed a block it did not have. Popen succeeding only means
the binary launched; logind can still refuse -- the guest's polkit denies a
non-interactive session outright ("Failed to inhibit: Access denied", exit
1) while the alarm cheerfully logged the power key as blocked.
take_power_key_inhibitor() now waits INHIBIT_CONFIRM_SECONDS and confirms
the child survived, returning None and logging the refusal otherwise.
2. An unsigned session record was written and then rejected on read. With no
HMAC key on the machine (/etc/gatelock absent, as in the guest),
verify_entry_hmac returns False for a *missing* key exactly as it does for
a forgery -- so every session record was unreadable and item 2's power-off
defence silently did not exist. An entry with no "hmac" field is now
honoured with a loud warning; a present-but-wrong signature is still
rejected, since that one really is tampering.
Two harness traps worth recording, both hit while building it:
- `vm run` reports the *VM's* verdict, not the guest command's exit code, so a
guest script exiting 1 still gives rc=0. A gate built on that fails OPEN.
guest_cases.sh prints ALL-CASES-PASSED only when every case passed and the
harness greps for it; verified by breaking a case on purpose and watching
the gate go red.
- `vm share`/`vm repo` key off the path basename, so REPO_DIR must be absolute
and symlink-resolved. A relative "scripts/vmbox/../.." makes rsync write
into /home and fail with permissions errors that look like a guest fault.
Guest checks live in real .py files beside the shell, not heredocs, so they
get linted like everything else -- ruff's autofix gutted an early heredoc
version (stripped the assignments AND the print) exactly as TODO-hardening
warned, and only running it showed the file had stopped producing output.
Also drops three stale fan references the guest install run surfaced:
install.sh still announced "Fans will ramp to 100%" and README.md still
described the fan ramp, three commits after item 14 deleted the feature.
Splits carried: test_inhibit.py -> + test_inhibit_cleanup.py,
test_alarm_session.py -> + test_alarm_session_refire.py.
455 tests pass, 100% branch coverage. Harness: 8/8 cases, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh