A manual workout may be dated today..today+7 days (no past dates). A future
entry is held out of log.json and earns nothing until its date; the first
sync/lock/reset pass on that day ingests and credits it like a workout logged
that day.
- ingest_manual_records defers records dated after the local day and counts
the budget (2/7d, 10/30d) against the workout's own date over logged credits
plus pending future records (a pending record never rejects itself)
- PC-logged future entries (Tk dialog, --log-manual-workout --date) go to a
durable pending ledger re-published on every push (a one-shot publish to
devices/pc is overwritten by the next push); tombstones win, entries leave
the ledger once logged or deleted
- one shared date helper (validate_workout_date) used by the dialog and CLI;
the form's date field rides in the sport row to keep the lock screen fitting
- poke start_the_day also ingests due local entries after the daily reset
- PC admission rule pinned to the phone's by the shared budget fixture
(planned.expected_by_date)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mmTBiMpvyfLuEAXc6nDYT
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
A 5+ workout week no longer banks (count - 4)h, and the every-4-week streak
milestone +1h is gone. The streak counter and the 09:00 early-bird extension
stay. The applying path is untouched, so 2026-W41's already-banked 2h keeps
applying through Sunday 2026-10-11; old weekly_shutdown_bonus_hours keys load
and are preserved as-is. The next-week status preview no longer promises
bonus hours.
Claude-Session: https://claude.ai/code/session_01WfVGhY8XcsQsC7qz9UU8xb
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`sync_now()` -- what workout-sync.timer runs every 15 minutes -- only ever
ingested MANUAL records. The session reader `pull_synced_workout()` was
reachable only from `_verify_phone_workout`, i.e. the interactive lock/status
UI, so a finished session earned nothing until a lock screen happened to open.
That was survivable while the phone was the only device recording sessions:
the same lock screen that demanded the workout also verified it. The Linux
build finishes sessions with no lock screen involved at all, so the credit
would simply never arrive.
`pull_all_session_records()` is the session counterpart to the existing
`pull_all_manual_records()` -- `pull_synced_workout` returns only the single
newest session for the live unlock check, which cannot backfill a week.
New type `pc_workout_verified` rather than reusing `phone_verified`: it is
machine-checked in exactly the same way (same WorkoutSession payload, same
set/rep and duration validation), but there is no phone involved, and writing
"phone_verified" into an HMAC-signed log would make the log lie about its own
provenance.
Ingestion re-derives the verdict instead of trusting the record's `succeeded`
flag -- the record arrives over the network and this is the thing handing out
unlock credit. Every rejection logs at WARNING with an actionable reason, per
the repo's no-silent-failures rule.
One real leak fixed along the way: formatting the duration with `.0f` rounded
34m59s up to "35 min", which is precisely the hidden accept bar -- the message
would have told the user the real threshold while rejecting them at it. It now
truncates, so the advertised 40 stays the only number shown.
Verified against live data: ingests the 4 real synced sessions, and a second
run ingests 0 (the write chokepoint dedups by workout_id), so the repeating
timer cannot double-credit. Suite green at 100% branch coverage, 1046 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfJC1Wu1SEgJ6X49k5Jqtg