Markdown across every repo had accumulated a dozen ad-hoc names (refactor_claude_todo.md x23, DESIGN_AUDIT_TODO.md, NEXT_SESSION, HANDOFF, PLAN, SPEC) with nothing distinguishing a live task from a finished record. An audit found docs asserting work was pending that shipped months earlier, and one asserting completion while the work was open. Four namespaces now: README.md, CLAUDE*.md, DOCS*.md, TODO*.md. Task files carry the literal "REMOVE ME AFTER FINISH" and are deleted when the work lands. Enforced bidirectionally -- a TODO without the marker fails, AND any file carrying the marker that is not named TODO*.md fails. Without the reverse rule a task can be renamed into the permanent namespace and quietly live forever, which is the failure being fixed. md_naming/ mirrors the file_length topology and imports its tables (EXCLUDED_DIRS, THIRD_PARTY_REPOS, VENDORED_ANYWHERE, GENERATED_PATTERN) rather than restating them, so the two gates cannot disagree about the same file. 71 tests, 100% branch coverage. stale.py warns on TODOs past 90 days but never fails: no machine can prove a task is finished, so the honest mechanism is a nudge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
9.9 KiB
Close the feedback asymmetry in workout_app
findings (4, ranked by value over effort)
- Finishing a workout gets no cue — while the rest timer gets sound and an 800ms vibration. The app celebrates waiting and ignores achieving. All infrastructure is already present; only the call is missing.
- Set completion has no haptic — the app's most repeated action, silent.
- The fleet's only animation is an inline magic number —
Duration(milliseconds: 200)inexercise_tile_rows.dart. - No sound opt-out exists — the SQLite settings table is ready, but no
public accessor and no
SwitchListTileanywhere. Step 2 only.
what
workout_app is the only app in kuhy's fleet that can already play a sound and vibrate — and it spends that capability on the wrong moment. Verified 2026-08-16:
- Rest-timer expiry gets a full cue:
_playBreakEndCue()inlib/screens/workout_screen_session.dart(~:128-145) playsassets/sounds/break_end.mp3and firesVibration.vibrate(duration: 800). - Finishing an entire workout gets nothing.
_persistFinishedWorkout()inlib/screens/workout_screen_finish.dart(~:99-108) ends by showingWorkoutSummaryDialogwith no sound, no haptic, no animation. - Completing a set — the core repeated action of the whole app — gets no
haptic either. The
GestureDetectoratlib/widgets/exercise_tile_rows.dart(~:145) toggles state silently.
So the app celebrates waiting and ignores achieving. Fix the asymmetry: the moments worth reinforcing are completing a set and finishing a workout.
This app also contains the fleet's only animation — a lone
AnimatedContainer(duration: Duration(milliseconds: 200)) at
exercise_tile_rows.dart ~:146-147, with an inline magic number. Replace that
number with a motion token while you are in the file.
where
Repo: ~/screen-locker. App: ~/screen-locker/stronglift_replacement/workout_app
(a Flutter app nested inside the screen-locker repo; that directory contains no
Python — only the app and two design docs).
Primary:
lib/widgets/exercise_tile_rows.dart— set-completionGestureDetector~:145, theAnimatedContainer~:146-147.lib/screens/workout_screen_finish.dart—_persistFinishedWorkout()~:99-108.lib/widgets/workout_summary_dialog.dart—WorkoutSummaryDialog~:10.lib/screens/workout_screen_session.dart—_playBreakEndCue()~:128-145, the existing cue to model the new ones on. Note its.catchErroronlydebugPrints (~:131-137).lib/screens/workout_screen.dart—_audio = AudioPlayer()~:78,vibrationimport ~:9.lib/widgets/rep_circle.dart—RepCircle~:34, per-set completion indicator.
Progress/history surfaces (already rich; animate rather than rebuild):
lib/screens/history_screen_charts.dart—_ProgressStatsCard~:71,_StreakRow~:139,_WeightChart~:192.lib/screens/history_screen_painter.dart—_ChartPainter~:9,paint()~:46,shouldRepaint~:121 (static; no animation today).- Streak data:
successStreak/failStreakonExerciseState(lib/services/storage_service.dart~:42-46).
Settings (for the sound opt-out):
lib/screens/settings_screen.dart—build()~:166,ListView~:174-208. Parts:settings_screen_rows.dart,settings_screen_sections.dart,settings_screen_exercise_sections.dart,settings_screen_thresholds.dart,settings_screen_widget.dart,settings_screen_actions.dart. NoSwitchListTileexists — model a new section on_SyncSection(settings_screen_sections.dart~:14) or_OfflineBackupSection.- Store: SQLite, not SharedPreferences. There is a ready-made key/value table
— schema
lib/services/storage_service_schema.dart~:34-36 (CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL), migration ~:64-65), accessorslib/services/storage_service.dart~:122-137 (_getSetting/_setSetting, both private — add a public typed wrapper like the existingsetLastWorkoutType). Already backed up/restored vialib/services/storage_service_backup.dart~:23, :43, :156-163.
Theme: lib/ui/theme.dart — local hand-copy, no design_system dependency
(buildAppTheme() ~:12, AppStatusColors ~:76, AppSpacing ~:111,
AppRadius ~:133, AppTextSize). Wired at lib/main.dart ~:115.
must
Step 1 — motion + haptics (no new dependency).
- Add
HapticFeedback.selectionClick()on set completion — the tap atexercise_tile_rows.dart~:145. This is the app's most repeated action; it should feel like flipping a physical switch. Fire on tap, not after the write. - Add a completion cue when a workout finishes — haptic plus a visible
arrival for
WorkoutSummaryDialog, rather than it appearing instantly. - Replace the inline
Duration(milliseconds: 200)at ~:146-147 with the motion token from prompt 01, transcribed intolib/ui/theme.dart. - Animate the streak/stat numbers on the history screen to their new values
instead of snapping.
_ChartPainter.shouldRepaintcurrently returns a static result — leave the chart itself alone unless animating it is cheap. - Honour
MediaQuery.of(context).disableAnimations— durations collapse to zero; haptics and sound still fire. VIBRATEis already granted (android/app/src/main/AndroidManifest.xml~:10) andvibration: ^3.1.0is already a dependency. PreferHapticFeedbackfromflutter/services.dartfor short UI taps and reserve thevibrationpackage for the long timed buzz it already does well.
Step 2 — sound (separate, independently revertable).
- Add a finish-workout cue asset alongside
assets/sounds/break_end.mp3(declared atpubspec.yaml~:52-53 — this is the fleet's onlyassets:block, so you are adding a file, not the block). - Sound ships on, with an opt-out. Add a public
getSoundEnabled()/setSoundEnabled(bool)onStorageServicewrapping_getSetting/_setSetting(store'true'/'false'), then a settings section with aSwitchListTile. - The toggle must silence the new cues and the existing break-end cue alike — one switch, not two behaviours.
Both steps:
-
must not: reward the weight or rep number. The cue fires on logging a set and finishing a workout, not on hitting a PB or beating last week. Rewarding the number pushes toward misreporting; rewarding the record keeps the data honest. A deloaded set gets the same confirmation as a PB.
-
must not: fabricate the streak.
successStreak/failStreakcount real outcomes; display them as they are. -
must not: add a cue to deleting a set or workout.
-
must not: silently swallow an audio error. The existing
.catchError→debugPrintat ~:131-137 is the local precedent; do not make it quieter. -
must not: break the 100% coverage gate — new public methods on
StorageServiceneed tests. See the comments atsettings_screen_sections.dart~:1-11. -
must not: exceed the 250-line cap on any file.
-
optional: a subtle near-miss cue on the history screen when a streak is one workout short of a milestone — legitimate only because the number is true.
done
- Completing a set produces an immediate haptic on the phone.
- Finishing a workout produces a haptic and a visible dialog arrival; step 2 adds a sound.
- No inline animation duration remains in
exercise_tile_rows.dart. cd ~/screen-locker/stronglift_replacement/workout_app && flutter analyzeclean.flutter testpasses and coverage stays at 100%.- With OS "remove animations" on, the app works and durations are zero.
- Step 2: the settings toggle flips, persists across restart, and silences both the new cues and the existing break-end cue.
verify
On the phone.
adb devices # confirm 23181JEGR08034
cd ~/screen-locker/stronglift_replacement/workout_app
flutter build apk --release
adb install -r build/app/outputs/flutter-apk/app-release.apk
Never flutter install, never adb uninstall, never pm clear — those wipe the
SQLite workout history. adb install -r preserves it.
Then run a real set on the device: tap a set complete, finish a workout, and report what the phone actually did. Haptics and sound are physical outcomes — state what you felt and heard; do not infer them from the code.
read first
lib/screens/workout_screen_session.dart~:128-145 —_playBreakEndCue(), the working cue. Your new cues should look like it.lib/widgets/exercise_tile_rows.dart— the set-completion tap and the fleet's onlyAnimatedContainer.lib/services/storage_service.dart~:122-137 —_getSetting/_setSetting, andsetLastWorkoutTypeas the public-wrapper pattern.lib/screens/settings_screen_sections.dart~:1-11 — the coverage/line-cap comments, and_SyncSectionas the section pattern.~/utils/unified-design-system/motion.md— motion/haptic vocabulary from prompt 01. Prompt 01 must have run first.
context you would otherwise rediscover
- This app has no
design_systemdependency;lib/ui/theme.dartis a hand-transcription of the shared tokens. Motion tokens must be transcribed there, matching the existing convention. Do not add the dependency here. shared_preferencesis a dependency but is used only for the device id (lib/services/sync_device_id.dart~:37-38). Settings go in SQLite.- Non-motion
Durations that are not animation and must not be touched: rest tickers (workout_screen.dart~:91,workout_screen_breaks.dart~:20,workout_screen_session.dart~:37 — 1s), settings debounces (settings_screen.dart~:140, :148 — 600ms),main.dart~:56 (20s). - The app syncs to the screen-locker Python side, which treats
screen_locker/workout_log.jsonas the single source of truth. Do not change what counts as a workout — enforcement depends on it, and it is out of scope. - The repo enforces a 250-line file cap and bans
# noqa; ruff runsselect = ["ALL"]on the Python side.
REMOVE ME AFTER FINISH