A sick day may keep some lockers active: a locker stands down iff the day
is sick and the locker is not in that day's keep set. New record field
`keep` = [sorted, de-duplicated locker ids, hlc].
- add/remove write state, actor and keep on one tick (keep=[] on a clear),
so a stale keep cannot survive a clear + re-add.
- set_kept_lockers re-stamps only keep and actor; state keeps its clock, so
a keep edit never re-asserts sick over a concurrent clear. Ticks are
issued after every clock already on the record (clock-skew safe).
- Reading: no keep (0.1.0) or keep older than state -> keep nothing;
malformed keep -> every locker kept (fail closed, logged); unknown ids
ignored.
- API: stands_down, kept_lockers, set_kept_lockers, add_sick_day(keep=),
SickDay.keep, LOCKER_* / LOCKERS. is_sick_day unchanged.
- CLI: add --keep, keep DAY [LOCKER ...], list shows keep, check --locker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
sickdays: per-date pool, no cap or reason, add/remove today or later only,
import_legacy for the migration. LWW state flip per date (no tombstones),
Firebase RTDB sickdays-sync/devices/<uuid>/sick_days.json, HMAC audit trail.
Readers fail closed (unreadable pool = not sick, logged); writers refuse a
corrupt pool. Free-day cross-check via a lazily imported freedays predicate.
earned_time: resolve(..., sick=True) floors the final shutdown at 20:00
(capped at the ceiling); gaming, terms and base unchanged. Result dataclasses
moved to _resolution.py for the line cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry