Painted chrome becomes icons or symbols; old words stay as
spoken labels or code comments. OS window titles kept. Tests
build today from the local day.
Todo: ed07cff3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sick day is now a date in the shared sickdays pool (utils sickdays 0.1.0)
and nothing else. Removed: the 7/30/90-day budget, the escalating lockout
countdown, the justification form, the commitment prompt and its double
debt, the workout-debt counter, the -60 min shutdown cut and its
sick_day_state.json, the post-sick-day --verify-workout mode, the sick
auto-upgrade, scripts/revoke_sick_day.py and their tests.
- Lock screen: "I'm sick" on the retry, no-phone and both Tue-Thu screens;
one tap -> sickdays.add_sick_day(today) -> sick floor -> best-effort
background sickdays.sync_quietly -> unlock. Refusals (free day,
unreadable pool) are shown and the lock stays.
- Shutdown: the reset resolves with earned_time.resolve(..., sick=True)
(max(20:00, ladder), capped 23:00) and saves the sick lift with the grace
lift, so later credits absorb it instead of stacking on 20:00. A day
marked after the reset is lifted by the new _sick_floor live pass (every
5-min tick, --sync-only, --apply-bonuses, and the tap itself).
- Workout lock and grace floor stand down on a sick day; workouts that day
credit normally through the sync paths.
- wake-alarm workday_penalty.json is void when its issued_date or
penalty_date is a sick day.
- Status window, i3 line, CLI --status, MCP flags and web reasons read
sickdays.
- scripts/migrate_sick_history.py imports sick_history.json's dates via
sickdays.import_legacy, verifies and removes the file; idempotent, run by
install.sh.
- Pins: earned-time 0.11.0, freedays 0.4.0, sickdays 0.1.0 (tags pending);
pre-commit 4.7.0, ruff 0.17.1 (freshness gate).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
Fifth and last app onto freedays (~/utils/freedays). screen-locker is the
one that already had this idea: scheduled_skips.json was a bare list of
dates, checked first in the lock chain, that only this app could see.
That is exactly what the shared pool generalises, so the chain now asks
freedays and a day taken here also stands down diet-guard, wake-alarm,
leetcode-guard and home-guard.
Sick days are deliberately untouched. Their rolling windows (1/7d, 3/30d,
10/90d), the 120-character justification and the escalating countdown all
exist to make a sick day *hard* to take. Free days are the opposite --
declared ahead, no reason, consecutive runs allowed. Merging the two
policies would have had to ruin one of them, so only the storage of the
frictionless half moved.
This fixes a real bug in passing. is_scheduled_skip_today compared
against a UTC day while add_scheduled_skip.py wrote a local one, so a
skip added near midnight could name a date the chain never matched.
freedays owns "what is today" for both sides now, in local time, and the
disagreement cannot recur.
scripts/migrate_scheduled_skips.py moves the existing dates across; it is
idempotent and has a --dry-run. Already run here: the 8 real skips
(2026-05-19/20/21, 05-29, 05-30, 08-17, 08-22, 08-29) are in the pool and
count against 2026, leaving 27 of 35. The original file is gitignored and
was left on disk untouched.
add_scheduled_skip.py becomes a thin wrapper over freedays rather than
being deleted, because arm.sh calls it and the name is in muscle memory.
Left as-is it would have kept writing to a file nothing reads.
conftest.py went over the 250-line cap, so the two fixtures that keep the
suite off real user state moved to _real_state_fixtures.py. They belong
together and neither subsumes the other: _isolate_home covers paths
resolved from Path.home() at call time, while the freedays redirect
covers paths bound into module constants at import time, which
redirecting home afterwards cannot reach.
requirements.txt also takes pytest-randomly 5.0.0, ruff 0.16.6 and
types-requests 2.33.0.20260906, and the pylint hooks' gatelock/crdt-sync
pins move off v0.7.1/v0.9.0 -- pip refuses two direct URLs for one
distribution, so those stale pins would have made the hook environments
unresolvable rather than merely old.
1256 tests, 100% branch coverage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading against 3.10 -- three tools judging an interpreter nothing
runs any more, and ruff withholding the pyupgrade fixes that only become safe
at the newer target. `pin_ci_python.py` rewrites exactly these three knobs.
What the stale target was hiding: 118 sites on `datetime.timezone.utc` instead
of `datetime.UTC` (UP017), two classes still written with `Generic[...]` rather
than PEP 695 syntax, and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That last one makes the source
3.14-only at the syntax level, which is what `requires-python = ">=3.14"`
already promised; kuhy chose it deliberately over holding ruff a release back
(2026-08-29).
The PEP 695 fix needed a hand: ruff rewrote `class WidgetGroup(Generic[_W])`
to `class WidgetGroup[W: tk.Misc]` but left the body annotated with the
module-level `_W`, a *different* type variable -- mypy caught it as "a function
returning TypeVar should receive at least one argument containing the same
TypeVar". Renamed the body to the new parameter, gave `FrameGroup.__init__`
the concrete `list[tk.Frame]` it always wrapped, and deleted the two now-dead
module-level TypeVars.
ruff, ruff-format, mypy --strict and both pylint passes clean at the new target.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
_compliance_state.py was 399 lines and status_view.py 396.
_compliance_state.py becomes three acyclic modules:
_compliance_predicates.py the five leaf predicates + the gatelock
log_integrity imports
_compliance_trace.py PredicateResult / AutoUpgradeOpportunity /
LockExplanation, _stage, and
describe_auto_upgrade_opportunity
_compliance_state.py explain_lock_decision, importing both
Two-module shapes were all cyclic here: _compliance_state must export the
engine while owning the predicates it calls. Three modules break that with no
deferred import, no forwarder and no loss of the typed signature; every name
is re-exported, so _status_data.py and _log_mixin.py are untouched.
Because the hmac helpers now live in _compliance_predicates, 29 patch targets
across 5 test modules move with them -- patching a name where it is no longer
read would silently stop taking effect.
status_view.py's seven _section_* render methods become StatusSectionsMixin in
_status_sections.py, composed into StatusWindow alongside its existing mixins.
Test count goes 975 -> 976: test_no_popup_widgets parametrizes over
PACKAGE_DIR.glob("*.py"), so _status_sections.py is now covered by the
no-popup safety check too. Coverage stays 100%, pylint holds at 8.70/10, and
`screen_lock --status` renders the real week unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
Adds a rate-limited manual-workout entry point (StatusWindow + locked
retry screen) for activities like table tennis that ADB/RunnerUp can't
verify, gated by a 2/7d + 5/30d budget and a detailed evidence form
rather than free text. Full credit toward the weekly minimum, debt
clearing, and commitment prompt, matching phone/RunnerUp verification.
Also stops tracking personal runtime state (workout_log.json,
sick_history.json, extra_benefits_state.json, scheduled_skips.json,
shutdown_base.json, early_bird_pending.json) in git — these are
regenerated locally and never belong in a public/private repo's
history.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbPcvAvt1JhiRzEyPDE5dA