diff --git a/docs/superpowers/evidence/drop-alarm-leg-morning-routine-2026-08-30.json b/docs/superpowers/evidence/drop-alarm-leg-morning-routine-2026-08-30.json new file mode 100644 index 00000000..8da0e633 --- /dev/null +++ b/docs/superpowers/evidence/drop-alarm-leg-morning-routine-2026-08-30.json @@ -0,0 +1,43 @@ +{ + "intent": "Keep the morning workout lock running after the PC's wake alarm was deleted upstream, by removing the orchestrator's now-dangling alarm leg in the same change.", + "scope": [ + "python_pkg/morning_routine/_orchestrator.py", + "python_pkg/morning_routine/morning-routine.service", + "python_pkg/morning_routine/install.sh", + "python_pkg/morning_routine/tests/test_orchestrator.py", + "Non-goal: any change to screen_locker or to the workout lock itself", + "Non-goal: removing the orchestrator, which wake_alarm's TRIGGER_TARGET_UNIT still names" + ], + "changes": [ + "Removed ALARM_MODULE, _run_alarm() and the --with-alarm flag; main() now runs only the workout lock. wake_alarm._alarm no longer exists after kuhyx/wake-alarm retired the PC's waking role.", + "morning-routine.service drops --with-alarm; its Description and comments now name wake-alarm-trigger.timer as what starts it, instead of the deleted systemd-sleep hook. Environment=DISPLAY=:0 kept, because workout-locker.service needs it even though the comment credited it to the alarm's Tk window.", + "install.sh header no longer promises rtcwake/python-kasa/sleep-hook prerequisites that upstream no longer installs.", + "Bumped hypothesis 6.165.10 -> 6.167.0 in meta/requirements.txt to clear a pre-existing red dependency-freshness baseline on main, unrelated to this change but blocking the commit." + ], + "verification": [ + { + "command": "/usr/bin/python3 -m pytest python_pkg/morning_routine/tests/ -q", + "result": "pass", + "evidence": "7 passed. Alarm-leg tests replaced by test_main_runs_the_workout_lock and test_production_flag_is_still_accepted (systemd passes --production)." + }, + { + "command": "shellcheck python_pkg/morning_routine/install.sh", + "result": "pass", + "evidence": "clean" + }, + { + "command": "grep -rn 'with-alarm|ALARM_MODULE|_run_alarm' python_pkg/morning_routine/", + "result": "pass", + "evidence": "no matches; the flag is gone from source, unit file, installer and tests together" + } + ], + "risks": [ + "The installed ~/.config/systemd/user/morning-routine.service is a copy, not a symlink, so it keeps the old --with-alarm ExecStart until install.sh is re-run. Until then the orchestrator receives an unknown flag and argparse exits 2, so the workout lock would not run.", + "This is the exact failure class the change exists to prevent: on 2026-05-28 a stale module reference here left the workout lock dying with ModuleNotFoundError for a month while the alarm half kept working and hid it." + ], + "rollback": [ + "git revert the commit, then re-run bash python_pkg/morning_routine/install.sh to restore the unit file.", + "Rollback is only safe together with reverting kuhyx/wake-alarm's deletion commit: restoring --with-alarm while wake_alarm._alarm is absent reintroduces the dangling reference.", + "After rollback: systemctl --user cat morning-routine.service and confirm the ExecStart matches the repo copy." + ] +} diff --git a/meta/requirements.txt b/meta/requirements.txt index 7b259004..5373af45 100644 --- a/meta/requirements.txt +++ b/meta/requirements.txt @@ -38,7 +38,7 @@ gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.2#subdirectory=gatel genanki==0.13.1 geopandas==1.1.4 howlongtobeatpy==1.0.23 -hypothesis==6.165.10 +hypothesis==6.167.0 importlib-metadata==9.0.1 interrogate==1.7.0 # isort is deliberately not pinned: nothing here imports it (ruff's isort diff --git a/python_pkg/morning_routine/_orchestrator.py b/python_pkg/morning_routine/_orchestrator.py index eb8a6599..84fbc21b 100644 --- a/python_pkg/morning_routine/_orchestrator.py +++ b/python_pkg/morning_routine/_orchestrator.py @@ -1,27 +1,21 @@ -"""Orchestrate the morning wake/workout flow as one sequential routine. +"""Run the morning workout lock, started at the phone's alarm time. -The wake alarm (``wake_alarm``, https://github.com/kuhyx/wake-alarm) and the -workout screen lock (``screen_locker``, https://github.com/kuhyx/screen-locker) -used to run as two independent ``graphical-session.target`` user services, -each opening its own fullscreen ``-topmost`` Tk window. On a wake morning they -could grab the screen at the same time, so the alarm could end up hidden -behind the workout lock (or vice versa). +This used to sequence two fullscreen Tk windows: the PC wake alarm first, then +the workout screen lock. The PC no longer wakes anyone -- the phone does, and +`wake_alarm._alarm` has been deleted -- so there is only one leg left and no +collision left to prevent. -This orchestrator makes them one coherent flow by running them as **sequential -subprocesses**: the alarm runs first and owns the fullscreen until it is -dismissed, then the workout lock runs. Only one fullscreen window is ever alive -at a time, so they can never collide. Each subprocess still self-gates (the -alarm only fires on alarm days when undismissed; the lock exits if a skip was -earned or the workout is already logged), so this is safe to run on every wake. +What still points here: `wake-alarm-trigger.timer` ticks every minute and +starts `morning-routine.service` at the synced alarm time +(`wake_alarm._constants.TRIGGER_TARGET_UNIT`). Keeping this indirection means +that unit name stays true, and the workout lock keeps its own gates. -Both ``wake_alarm`` and ``screen_locker`` are pip-installed into system -Python's user site-packages (each repo's own install.sh does this), so -``python -m `` resolves them with no extra ``PYTHONPATH``/``cwd`` -plumbing here. +``screen_locker`` is pip-installed into system Python's user site-packages by +its own install.sh, so ``python -m `` resolves it with no extra +``PYTHONPATH``/``cwd`` plumbing here. Usage: - python -m python_pkg.morning_routine._orchestrator --with-alarm # resume - python -m python_pkg.morning_routine._orchestrator # lock only + python -m python_pkg.morning_routine._orchestrator """ from __future__ import annotations @@ -36,7 +30,6 @@ from python_pkg.shared.logging_setup import configure_logging _logger = logging.getLogger(__name__) # Modules invoked as ``python -m --production``. -ALARM_MODULE: str = "wake_alarm._alarm" WORKOUT_LOCK_MODULE: str = "screen_locker.screen_lock" @@ -59,11 +52,6 @@ def _run_module(module: str) -> int: return result.returncode -def _run_alarm() -> int: - """Run the wake alarm and block until it is dismissed (or self-exits).""" - return _run_module(ALARM_MODULE) - - def _run_workout_lock() -> int: """Run the workout screen lock after the alarm has been dealt with.""" return _run_module(WORKOUT_LOCK_MODULE) @@ -72,11 +60,6 @@ def _run_workout_lock() -> int: def _parse_args(argv: list[str]) -> argparse.Namespace: """Parse CLI arguments for the orchestrator.""" parser = argparse.ArgumentParser(description="Unified morning routine.") - parser.add_argument( - "--with-alarm", - action="store_true", - help="Run the wake alarm before the workout lock (used on resume).", - ) parser.add_argument( "--production", action="store_true", @@ -86,14 +69,9 @@ def _parse_args(argv: list[str]) -> argparse.Namespace: def main() -> None: - """Entry point: optionally run the alarm, then always run the workout lock.""" + """Entry point: run the workout lock.""" configure_logging() - args = _parse_args(sys.argv[1:]) - # Alarm first so it owns the fullscreen and escalates until dismissed; only - # then hand off to the workout lock. Running them in this order in a single - # process guarantees they never fight for the screen. - if args.with_alarm: - _run_alarm() + _parse_args(sys.argv[1:]) _run_workout_lock() diff --git a/python_pkg/morning_routine/install.sh b/python_pkg/morning_routine/install.sh index 1c2672ef..3b09a260 100755 --- a/python_pkg/morning_routine/install.sh +++ b/python_pkg/morning_routine/install.sh @@ -1,16 +1,15 @@ #!/bin/bash -# Install the unified morning routine: wake alarm -> workout lock as one flow. +# Install the morning routine: the workout lock, started at the alarm time. # # What it does: -# 1. Installs morning-routine.service (user service, started on resume). -# 2. Disables the standalone wake-alarm.service autostart: the orchestrator -# runs the alarm now, and this also removes its evening-login firing quirk. +# 1. Installs morning-routine.service (user service, started at alarm time). +# 2. Clears the retired standalone wake-alarm.service left by old installs. # 3. Leaves workout-locker.service + the early-bird timer for login / 08:30. # -# Prereq: run wake_alarm's own install.sh first (https://github.com/kuhyx/wake-alarm) — -# it installs the rtcwake/fan sudoers entries, python-kasa, and the -# systemd-sleep hook that starts morning-routine.service on resume. This -# script does not duplicate that hook install. +# Prereq: run wake_alarm's own install.sh first (https://github.com/kuhyx/wake-alarm). +# It installs wake-alarm-trigger.timer, which ticks every minute and starts +# this unit at the time the phone synced. The PC no longer wakes anyone, so +# there is no longer a shutdown wrapper or systemd-sleep hook involved. set -euo pipefail @@ -27,17 +26,17 @@ cp "$SERVICE_SRC" "$SYSTEMD_USER_DIR/morning-routine.service" systemctl --user daemon-reload echo " Installed to $SYSTEMD_USER_DIR/morning-routine.service" -# 2. Disable the standalone wake-alarm.service autostart (orchestrator owns it). -echo "[2/2] Disabling standalone wake-alarm.service autostart..." +# 2. Clear the retired wake-alarm.service, which no longer exists upstream. +echo "[2/2] Clearing the retired wake-alarm.service..." if systemctl --user cat wake-alarm.service &>/dev/null; then - systemctl --user disable wake-alarm.service 2>/dev/null || true - systemctl --user stop wake-alarm.service 2>/dev/null || true - echo " wake-alarm.service autostart disabled (alarm runs via orchestrator)" + systemctl --user disable wake-alarm.service 2>/dev/null || true + systemctl --user stop wake-alarm.service 2>/dev/null || true + echo " retired wake-alarm.service disabled" else - echo " wake-alarm.service not installed; nothing to disable" + echo " wake-alarm.service not installed; nothing to clear" fi echo "=== Installation complete ===" -echo "On resume the morning routine runs: wake alarm -> workout lock." +echo "wake-alarm-trigger.timer starts this at the phone's synced alarm time." echo "Test now:" -echo " python -m python_pkg.morning_routine._orchestrator --with-alarm --production" +echo " python -m python_pkg.morning_routine._orchestrator --production" diff --git a/python_pkg/morning_routine/morning-routine.service b/python_pkg/morning_routine/morning-routine.service index 71367d22..d894d53a 100644 --- a/python_pkg/morning_routine/morning-routine.service +++ b/python_pkg/morning_routine/morning-routine.service @@ -1,18 +1,18 @@ [Unit] -Description=Unified Morning Routine (wake alarm + workout lock) +Description=Morning Routine (workout lock) After=graphical-session.target [Service] Type=simple -# Started by the systemd-sleep hook on resume (NOT auto-started at login), so -# there is intentionally no [Install] WantedBy=graphical-session.target: the -# alarm must only run on a real wake, never on an ordinary evening login. +# Started by wake-alarm-trigger.timer at the phone's synced alarm time, so +# there is intentionally no [Install] WantedBy=graphical-session.target: it +# must only run at alarm time, never on an ordinary evening login. # DISPLAY/PYTHONPATH mirror workout-locker.service so Tk can open the X server; -# the short sleep lets the resumed session export DISPLAY first. +# the short sleep lets the session export DISPLAY first. Environment=DISPLAY=:0 Environment=PYTHONPATH=%h/testsAndMisc ExecStartPre=/bin/sleep 1 -ExecStart=/usr/bin/python -m python_pkg.morning_routine._orchestrator --with-alarm --production +ExecStart=/usr/bin/python -m python_pkg.morning_routine._orchestrator --production WorkingDirectory=%h/testsAndMisc Restart=on-failure RestartSec=2s diff --git a/python_pkg/morning_routine/tests/test_orchestrator.py b/python_pkg/morning_routine/tests/test_orchestrator.py index 2b423c0b..44344e58 100644 --- a/python_pkg/morning_routine/tests/test_orchestrator.py +++ b/python_pkg/morning_routine/tests/test_orchestrator.py @@ -5,10 +5,8 @@ from __future__ import annotations from unittest.mock import MagicMock, patch from python_pkg.morning_routine._orchestrator import ( - ALARM_MODULE, WORKOUT_LOCK_MODULE, _parse_args, - _run_alarm, _run_module, _run_workout_lock, main, @@ -41,13 +39,7 @@ class TestRunModule: class TestRunHelpers: - """Tests for _run_alarm and _run_workout_lock.""" - - def test_run_alarm_runs_alarm_module(self) -> None: - """_run_alarm delegates to _run_module with the alarm module.""" - with patch(f"{_ORCH}._run_module", return_value=0) as mock_run: - assert _run_alarm() == 0 - mock_run.assert_called_once_with(ALARM_MODULE) + """Tests for _run_workout_lock.""" def test_run_workout_lock_runs_lock_module(self) -> None: """_run_workout_lock delegates to _run_module with the lock module.""" @@ -59,44 +51,32 @@ class TestRunHelpers: class TestParseArgs: """Tests for _parse_args.""" - def test_with_alarm_flag(self) -> None: - """--with-alarm sets with_alarm True.""" - assert _parse_args(["--with-alarm"]).with_alarm is True - - def test_default_no_alarm(self) -> None: - """No flag leaves with_alarm False.""" - assert _parse_args([]).with_alarm is False - def test_production_flag(self) -> None: """--production is accepted.""" assert _parse_args(["--production"]).production is True class TestMain: - """Tests for main() sequencing.""" + """Tests for main().""" - def test_with_alarm_runs_alarm_then_lock(self) -> None: - """--with-alarm runs the alarm first, then the workout lock, in order.""" - manager = MagicMock() + def test_main_runs_the_workout_lock(self) -> None: + """The one remaining leg runs; the alarm leg is gone entirely.""" with ( - patch(f"{_ORCH}._run_alarm", manager.alarm), - patch(f"{_ORCH}._run_workout_lock", manager.lock), - patch(f"{_ORCH}.sys") as mock_sys, - patch(f"{_ORCH}.logging.basicConfig"), - ): - mock_sys.argv = ["orch", "--with-alarm"] - main() - assert [call[0] for call in manager.mock_calls] == ["alarm", "lock"] - - def test_without_alarm_runs_only_lock(self) -> None: - """Without --with-alarm, the alarm is skipped and only the lock runs.""" - with ( - patch(f"{_ORCH}._run_alarm") as mock_alarm, patch(f"{_ORCH}._run_workout_lock") as mock_lock, patch(f"{_ORCH}.sys") as mock_sys, patch(f"{_ORCH}.logging.basicConfig"), ): mock_sys.argv = ["orch"] main() - mock_alarm.assert_not_called() + mock_lock.assert_called_once() + + def test_production_flag_is_still_accepted(self) -> None: + """systemd passes --production; main must not choke on it.""" + with ( + patch(f"{_ORCH}._run_workout_lock") as mock_lock, + patch(f"{_ORCH}.sys") as mock_sys, + patch(f"{_ORCH}.logging.basicConfig"), + ): + mock_sys.argv = ["orch", "--production"] + main() mock_lock.assert_called_once()