Commit Graph

19 Commits

Author SHA1 Message Date
99302c2be7 Add todo_finish_dart_250_prompt.md and the Dart test splitter
A self-contained prompt for a fresh session to finish the last 10 files over
the 250-line cap, all Dart. It carries the parts that are expensive to
rediscover:

- scripts/split_dart_test.py, which works, plus the envelope it works in:
  one pass from the pristine file, no post-main() declarations, no local
  function declarations in the preamble. All four remaining test files sit
  outside that envelope, and the fix (hoist shared fakes into a
  _fixtures.dart sibling first, as its own commit) is written down.
- The three Dart constraints each of which cost a debugging cycle here:
  setState is @protected so it cannot be called from an extension; extension
  methods dispatch statically so a method a test fake overrides must stay in
  the class (this hung four tests on pumpAndSettle rather than failing an
  assertion); static members need qualifying inside an extension.
- 402 as the test-count invariant, and the note that dart analyze is a no-op
  on test/ so the count is the only signal there.
- That the local gatelock (0.5.0) is ahead of the pin (0.4.0), so the popup
  gate's failure here is environment drift and must NOT be "fixed" in code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:49:15 +02:00
d418994f6f Split storage_service.dart (781) into part extensions
storage_service.dart            the class: open, settings, active session
  storage_service_schema.dart     create / migrate / seed
  storage_service_exercises.dart  read+write a single exercise's row
  storage_service_sessions.dart   apply progression, record sessions
  storage_service_backup.dart     backup, restore, sync markers

Dart cannot continue a class body across files, so unlike history_screen these
are `extension`s in `part` files. Library privacy still applies inside a part,
so they reach `_db` and `_getSetting` unchanged and every caller still writes
`StorageService.instance.foo()` -- no public API change, nothing for
public_member_api_docs to demand.

Doing this exposed a bug in scripts/check_silent_failures_dart.py: its bare
`on Foo {` pattern (the catch clause form) also matched
`extension StorageServiceBackup on StorageService {`, so it reported the
declarations as swallowed exceptions. It now requires the try block's closing
`}` first. Verified both directions on a fixture: a real silent
`} on FormatException {` is still reported, the extension is not.

402 Dart tests still pass; flutter coverage still 100%; dart analyze clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:12:08 +02:00
b8cf634fa6 Record why the popup gate fails on a drifted gatelock
The RecoveryLoop call in HostileLock matches gatelock 0.4.0, which is what
requirements.txt pins and what the pylint hook installs. gatelock 0.5.0
regroups the five arguments into a RecoveryCollaborators object, so running
this gate against a locally-installed 0.5.0 fails with

    RecoveryLoop.__init__() takes 3 positional arguments but 6 were given

That is environment drift, not a defect in the check -- against the pinned
version the gate runs and passes both halves. Comment says so, and says how to
resync, so the next reader does not "fix" the code to match whatever happens to
be installed. (I nearly did; against the pin that edit would have broken CI.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 21:00:49 +02:00
76a4c6a0a7 Finish the Python side: scripts, conftest, and the last test modules
verify_lock_popup_safety.py (318) -> _xvfb_reexec.py      shared re-exec
                                       _popup_form_check.py HostileLock, ticks,
                                                            the real-form check
  verify_screen_fits.py (266)       -> uses _xvfb_reexec too
  conftest.py (329)                 -> _tk_mocks.py         the tk mock + the
                                                            module patch list
                                       _opt_in_fixtures.py  requestable fixtures
  three test modules                -> _part4/_part5/_part15 siblings

conftest keeps every autouse fixture. pytest applies autouse fixtures from
conftest only, so one moved to a sibling would silently stop running and take
its safety guard with it -- verified by asserting the fixtures are still
listed, not just that the suite is green. The five near-identical _isolate_*
fixtures collapse into one table-driven _isolate_state_files, which also picks
up the _shutdown_sick_state binding an earlier split created.

scripts/ becomes a real package so the shared helpers are importable. Both
gates now run as `python3 -m scripts.<name>`, including CI -- the path form put
scripts/ on sys.path instead of the repo root, and the alternative was a
try/except ImportError that this repo's no-silent-failures hook rightly
rejects. Each gate was run in its exact invocation form.

Note: verify_lock_popup_safety still fails on
`RecoveryLoop.__init__() takes 3 positional arguments but 6 were given`. That
is pre-existing -- confirmed by running it at HEAD in a clean worktree before
touching it -- a gatelock API drift, not this refactor. Left as found.

978 tests at 100% branch coverage. Gate: 41 -> 22 files, all remaining ones
Dart plus DESIGN_AUDIT_TODO.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
2026-08-15 20:57:29 +02:00
ee8f8bcf3a Add refresh_refactor_todo.py: self-correcting violations table
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 19:05:31 +02:00
5d4d491fb7 workout_app: add Google sign-in support for the Firebase sync account
Same pattern as todo and diet_guard_app: openFirebaseWithGoogle()
(google_platform.dart abstracts the plugin's platform-channel
dependence), isFirebaseConfigured() reading the stored session rather
than the account marker alone, and the settings screen offering
"Sign in with Google" ahead of the password form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdSp6Nbrs9xvsWkCMDN9a1
2026-08-14 10:56:16 +02:00
3a5f0962de Add a pre-commit gate against a local crdt_sync dependency_override
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
A dependency_override pointing crdt_sync at a local path is invaluable
while developing an unreleased library change, and silently catastrophic
if it survives the commit: local tests go green against code CI never
built. Enforce it can't ship instead of relying on someone remembering
to remove it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SH7XBkpEqQX4KghHdgx9T
2026-08-12 19:18:21 +02:00
aab51eed2d Carry progression through Firebase, and stop demanding storage permission
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Progression and the in-progress session now live at
screen-locker-sync/exercise_state/<name> and .../active_session — siblings of
devices/, not inside it. Both workout readers (_workout_sync.py:113 and
workout_sync_service.dart:307) list only screen-locker-sync/devices, so a
progression record is structurally invisible to them and cannot be miscounted
as a workout, which would silently grant unlock credit. Keyed on exercise name
because a reinstall mints a new per-install uuid.

Startup no longer calls requestStoragePermission(): that threw the user into a
system Settings page on every launch, for a permission that is now genuinely
optional. It only checks; the grant moved to an OFFLINE BACKUP section in
Settings, its sole caller.

The clobber guard keys on a persisted progression_synced_at marker, NOT
looksFreshlyInstalled(). The only production caller (_finishWorkout) writes a
workout_history row and last_workout_type BEFORE it pushes, so a freshness test
was already false by then and could never fire — a reinstall that connected
Firebase late (the uninstall wipes the keystore, so startup's pull finds no
account) would have overwritten every real remote record with factory defaults
on its first finished workout. Connecting in Settings now pulls immediately,
which closes that window. The same marker stops a pull reverting a deliberate
setExerciseWeight/resetExerciseToDefaults, which write no history at all.

Also adds a Dart silent-failure pre-commit hook. The Python one is
types:[python] and had never looked at a single Dart file — in the app where a
swallowed sync error costs the user a workout with nothing anywhere saying so.
It found 21 real silent swallows (keystore reads/writes, /sdcard mirror
failures, the HTTP bind, device-flow polls, and a .catchError((_) {})), all now
logging why.

Verified on the Pixel 6a: reinstall + deny storage + connect late restored
Bench 22.5→20.0, Row 22.5→25.0, Situp 30→31 reps with the remote copy
untouched; the RTDB device list stayed at exactly 3 entries and no progression
leaked into pull_synced_workout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JupKvZCNKRmEeRQY3G1unG
2026-08-10 16:54:21 +02:00
dd233b27aa Run the fit check as a module, so it works where nothing is installed
CI caught what this machine could not: the hook died with "No module named
'screen_locker'". The package is pip-installed here (editable, pointing at
this checkout), so running the script by path found it anyway; a fresh
checkout has no install, and `python3 scripts/x.py` puts only `scripts/` on
sys.path. `python3 -m scripts.verify_screen_fits` puts the repo root there
instead, which is the checkout in CI and here alike -- verified in a
throwaway worktree, where the module now resolves to the worktree's own
package rather than the installed one.

The xvfb re-exec runs the same way, with an explicit cwd so the child does
not depend on where the parent was invoked from.

pylint had the same blind spot for the same reason: it infers sys.path from
the files it is handed, so this script resolved its imports only when a
package file happened to be in the same batch -- passing or failing by what
else was staged. An init-hook puts the repo root on the path unconditionally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
2026-08-03 16:34:45 +02:00
a79c4fdd05 Stop the lock screen scrolling itself, and make every screen fit 768px
This morning the lock screen scrolled to the bottom and back with nobody
touching the keyboard, on the 1366x768 panel. Reproduced under Xvfb with the
real app: one programmatic focus_set() produced three scrolls,
frac 0.000 -> 0.436 -> 0.145. The cause is in gatelock (fixed in v0.4.0,
pinned here): the viewport followed every <FocusIn>, and this app focuses a
widget on every repaint. It now follows only focus the *user* moved, and a
screen that fits cannot scroll at all.

The other half is this app's own layout, because scrolling was covering for
screens that did not fit. The manual-workout form needed 1643px of a 768px
screen -- SUBMIT and BACK were simply off the bottom:

* Every font size and pad now goes through the design-system tokens rather
  than a literal. A positive size means *points* to Tk, so "36" rendered
  ~37% larger than the 36px intended; that alone was most of the overflow,
  on all 88 call sites.
* The form's cells put their label beside the field instead of above it, and
  the three reflection boxes share rows instead of taking one each.

Result, measured by the new scripts/verify_screen_fits.py (a pre-commit hook
now, so this cannot come back quietly): the form is 752px of 768px, and every
other screen -- including heat-skip, which had no viewport at all -- fits with
room to spare.

Also fixed, all found by measuring rather than by reading: content that fits
is centred again (the reset-to-top was cancelling it on every repaint), the
scrollbar only appears when there is something to scroll, and unlock_screen
moved to _unlock_view so screen_lock.py stays under the 400-line cap.

Tests: 896 pass. The focus/scroll helpers shipped in 3e77f4e had no coverage
at all; test_focus_and_scroll.py covers them now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
2026-08-03 15:57:48 +02:00
bc940f1112 Run the flutter hooks with the git hook's environment stripped
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
Same defect diet-guard's pre-push gate hit today, latent here: git exports
GIT_DIR to its hooks, and the flutter tool shells out to git to identify its
own SDK. With GIT_DIR set it reads THIS repository instead of /opt/flutter,
so `flutter --version` reports our own HEAD as the framework revision and our
URL as the repository, pub resolves the SDK as "0.0.0-unknown", and every
dependency constraint becomes unsatisfiable. flutter also caches that answer,
so the damage outlives the hook run and follows the next caller into another
repo.

Both flutter gates now clear the git variables at the point of use. Verified
by running the analyze hook with GIT_DIR set, which failed before and passes
now.

Also fixes the SC2295 shellcheck finding the coverage script had been carrying
-- it only surfaced now because no change had touched a shell file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:28:52 +02:00
c11e27f123 Pin gatelock 0.2.1, and stop the popup check passing when it cannot run
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
The lock-popup-safety job failed on its first CI run for two reasons, and only
one of them was the obvious one.

The obvious one: the script imports screen_locker directly rather than through
pytest, which would have put the rootdir on sys.path for it. CI now installs
the package.

The one worth having found: CI installs gatelock from the pinned tag, which
predated `holds_grab`, so every hostile tick died on an AttributeError --
inside a Tk `after` callback, where exceptions are swallowed and logged rather
than raised. The harness sailed past, printed OK and exited 0 while doing no
grab re-assertion whatsoever. A verification script that passes when it is
broken is worse than not having it.

So it now fails closed twice over: an explicit check that the installed
gatelock has the predicate at all, with a message naming the fix, and a count
of ticks that actually completed -- incremented after the work, so a raising
tick does not count. Fewer completed ticks than asked for is reported as "the
harness itself is broken, so this result proves nothing", not as a pass.

The pin moves to gatelock-v0.2.1, which carries both of yesterday's fixes: the
grab predicate that no longer steals the grab off our own posted menus, and
the recovery loop that no longer stops forever when a tick raises.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:18:04 +02:00
c0aae7d05b Make the lock overlay's sport selector usable, and focus its form fields
The manual-workout sport selector was a `tk.OptionMenu`, and while the screen
was locked it could not be changed. A posted Tk menu is a separate
override-redirect toplevel that takes the Tk grab for itself, and gatelock's
1s recovery tick attacks it from both sides: `_surfaces.enforce()` lifts the
lock surface over the menu, and `_reassert_grab()` read the grab sitting on
the menu as "the grab was lost" and yanked it back. The menu opened on click
and died within a second. On 2026-07-26 that forced an hour-long walk to be
logged through the table-tennis form, with the reason typed into `racket`.

Radio buttons instead. They are ordinary children of the surface, so they
lift with it and live inside the root's grab tree -- neither mechanism can
reach them. That is a structural guarantee, not a tuned workaround; the
alternative, teaching gatelock to skip its lift while a popup is posted,
would open a window where something could cover the lock. They are also the
first version of this control styled from the shared palette, where the
OptionMenu was default grey on the dark overlay.

Nothing on a lock surface may open a popup, so two halves enforce it. The
static half walks each module's AST for popup-creating names -- AST rather
than a source regex because the modules explain this bug in prose that names
the very widgets being banned. Its module list is derived ("imports tkinter",
minus a short reasoned exemption list) rather than hardcoded, so a new lock
screen is in scope the day it is written; a hardcoded list drafted today
would already have missed `_sick_dialog`. The behavioural half is
`scripts/verify_lock_popup_safety.py`: it builds the real form with real Tk,
replays both hostile behaviours for five ticks, and re-runs the original
failure so the mechanism stays demonstrated rather than described. It
re-execs itself under Xvfb -- a global grab on the developer's own session
would freeze it -- and a CI job runs it, because the pytest suite replaces
the whole `tk` module and so cannot host it.

Separately, no field ever took keyboard focus. Under a global grab that means
typing goes nowhere with no other window on screen to reveal why. The
manual-workout and sick-justification forms now each name their own first
field via a `focus=` flag; `on_focus_ready` genuinely has nothing to focus
(it fires before those screens exist) and its docstring now says that instead
of claiming the lock window has no typed input at all.

`scripts/fix_2026_07_26_sport.py` re-files that walk as sport "other",
rebuilt through the form's own draft/validate/build path so it carries
exactly the writer's key set, and re-signed. `start_time` and the date are
untouched so `workout_id` stays stable and the phone converges instead of
gaining a duplicate. Kept as the audit trail for the edit; the walk's details
come from the entry and `--details`, so nothing personal lands in a tracked
file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:08:08 +02:00
79879263ba Support multiple workouts per day; fix the dead sync; ban silent failures
The workout log stored ONE workout per calendar day, so a second same-day
workout was silently dropped and could never earn credit.

Multiple workouts per day:
- workout_log.json: {date: entry} -> {date: [entry, ...]}. Legacy entries are
  wrapped on read (_log_io.load_workout_log) so their HMACs stay valid — no
  re-signing; every write normalizes the file, migrating it progressively.
- New write chokepoint (_log_mixin.write_signed_entry) APPENDS and dedups by a
  stable workout_id, returning RecordResult so callers can tell a new workout
  from a duplicate. Re-verifying the same workout is a no-op.
- count_weekly_workouts is anti-gaming: each VERIFIED workout counts
  individually; self-reported manual entries count at most once per day. Four
  real workouts in one day satisfy the week; four manual entries count as one.
- Reward: the day's first counted workout gets the base +2h; an additional
  same-day VERIFIED workout gets +1h; an additional manual gets nothing. The
  old "+1h when weekly_count > 4" gate is replaced — weekly volume is the
  banked bonus's job and shouldn't be paid twice.
- Fixed: the unlock screen labelled the +1h "#{weekly_count - 4}", which can
  render negative under the new rule. Removed the dead runnerup_manual type.
- Flutter: the manual budget counts distinct DAYS, matching the PC.

PC->phone sync (it had never worked once):
- push_pc_manuals pushed from a side-store written at log-time, so anything
  logged before that feature existed could never publish. _manual_push now
  derives the crdt log directly from workout_log.json — the single source of
  truth — covering manual AND verified runs. The record id is the workout's own
  workout_id (derived for legacy entries to match the phone's format) and the
  HLC comes from the entry's own timestamp, so a re-push is byte-identical and
  the repo never churns.
- The phone reads every kind (readMergedWorkoutPayloads) and lists the PC's
  workouts under "SYNCED FROM PC". The manual budget still sees manual-only, so
  a verified run can't consume a self-report slot.
- Settings claimed "Connected." whenever a token STRING existed. It now
  verifies against the API on open, so a revoked token reports 401 instead of
  showing a green lie.

Never fail silently:
- 30 swallowed exceptions across 19 files now log WARNING with the concrete
  cause AND consequence — plus the token guards in _workout_sync that returned
  [] in total silence, the exact bug that hid the dead sync for weeks.
- New pre-commit hook (scripts/check_silent_failures.py) fails any except that
  neither re-raises nor logs loudly. ruff's S110 only catches `except: pass`;
  the real pattern here was `except: log.info(); return []`. Documented in
  CLAUDE.md.

Verified on real data: the Jul 13 run (pulled from its actual TCX over ADB) now
counts alongside that day's manual squash, and all three workouts are published
to kuhyx/syncs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fv8ULVt3Q67GwCFbM7doHB
2026-07-17 09:22:31 +02:00
5390f2043c Add CI-mirror pre-push gate + requirements-sync check
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Keeps CI green by closing the local↔CI gap that caused this session's
red runs: the installed pre-commit hook only checks staged files and the
dev env already has every dep, while CI runs `pre-commit run --all-files`
and installs a fresh env from requirements.txt.

- scripts/check_requirements_sync.py: static hook (pre-commit stage) that
  AST-scans the package's third-party imports and fails if any isn't
  declared in requirements.txt — the exact ModuleNotFoundError class that
  reddened three repos. Fast, runs at commit time.
- scripts/ci_mirror.sh: pre-push gate that reproduces both CI workflows —
  a requirements.txt-only venv (hash-gated, rebuilt only when
  requirements.txt changes) running `pre-commit run --all-files` +
  `pytest`. Replaces the old pytest-coverage pre-push hook (subsumed by
  the clean-venv pytest). A red result blocks the push; `git push
  --no-verify` overrides.
- .ci-mirror-venv/ gitignored; pre-push hook installed.

Verified: mirror runs green (747 passed, 100% cov); dep-sync check
catches a removed crdt-sync; hash-gate reuses the venv on repeat runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PMRRVVkeRyrSaaqueMZKG
2026-07-11 17:42:45 +02:00
494cc74bb5 Add read-only MCP server exposing workout/lock status
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Expose the screen-locker's compliance and lock-decision state as read-only
MCP tools (screen_locker._mcp) so Claude Code and its subagents can query
workout status without shelling out to the CLI or opening the Tk window.

- _mcp.py: FastMCP stdio server. Read-only tools get_status, get_summary,
  explain_lock, get_flags. Import leaf over stdout-free status functions
  (gather_status, format_summary_line, explain_lock_decision, predicates);
  logs to stderr. NO write/action tools by design — workouts are logged only
  from RunnerUp-verified TCX data, never from a caller's claim. No tool
  exposes the sync token or any HMAC key.
- .mcp.json + scripts/setup_mcp.sh: registration via a dedicated
  ~/.venvs/screen-locker-mcp interpreter; mcp is an optional-dep.
- 100% coverage of the new module; verified end-to-end over stdio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQGQj8Ne6TriBExv91nG8v
2026-07-10 21:28:09 +02:00
63489b07b2 Add comprehensive test suite, backup service, and linting to workout app
- Add sqflite_common_ffi + very_good_analysis; tighten analysis_options
- Add BackupService for JSON export/import of exercise state
- Add full test coverage: models, screens, services, widgets
- Add scripts/check_flutter_coverage.sh to enforce 100% line coverage
- Add docstrings to ExerciseState fields and storage service
- Minor fixes across screens, widgets, and sync/HTTP services

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VuiPt6GPWkxpLbJFrnfy8U
2026-06-28 08:11:43 +02:00
eec0662ef9 Add auto-fill RunnerUp scan, carrot bonuses, and --status interface
- Refactor RunnerUp verification: extract RunnerUpDbMixin (_runnerup_db.py),
  split _scan_and_fill_week_runnerup into a helper _try_fill_runnerup_for_date
  to keep cyclomatic complexity ≤10
- Generalise TCX lookup to any date in the ISO week (was today-only); all gap
  days Mon→today auto-filled on every startup and 08:30 timer firing
- Add _adjust_shutdown_time_by(): +1h per extra workout beyond the 4-workout
  minimum, capped at midnight (hour=24)
- Add _shutdown_base.py: daily reset of shutdown config to a stored base so
  the bonus doesn't silently accumulate across days
- Add _extra_benefits.py: streak tracking, skip credits (earn (n-4) credits
  for 5+ workout weeks), early-bird extension to 09:00 for eligible weeks
- Add --status mode (_status.py): non-locking CLI view showing per-day
  breakdown (✓/✗), RunnerUp auto-scan, bonus status, shutdown time, streak,
  skip credits, and early-bird status
- Hook carrot into _check_non_verify_exits: bonus applied whenever auto-fill
  pushes weekly count above the minimum
- Pass all pre-commit hooks (ruff, mypy, pylint, bandit, shellcheck,
  codespell, max-file-length); 508 tests at 100% branch coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017auyHmf2ZwQcDAwXaSo7KX
2026-06-28 08:08:35 +02:00
de2ef38fd0 refactor: enforce 400-line file limit with pre-commit hook and split outliers
Add scripts/check_file_length.py and a max-file-length pre-commit hook that
fails any Python/shell file exceeding 400 lines. Extract UIWidgetsMixin and
UIFlowsRelaxedMixin from screen_lock.py and _ui_flows.py respectively, and
split 6 oversized test files into part2/part3/part4 siblings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 21:19:18 +02:00