From b1ee78be648b9d00a2d440da74c76018c135d242 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 16 Aug 2026 21:12:52 +0200 Subject: [PATCH] Add dopamine-ux session prompts to prompts/ Copy of the canonical prompts in ~/utils/dopamine-ux/, distributed here so the repo can be opened and the prompt run directly. Derived from a survey of this repo plus "The Dopamine Architecture" (london-post.co.uk). Each prompt states its own completion protocol: when finished, the session deletes this copy in the implementation commit and logs a DONE row in ~/utils/dopamine-ux/00-INDEX.md. Partial completion is never recorded as done. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FdApz7EZ1zDfvtvvi1idDv --- prompts/dopamine-ux-04-screen-locker.md | 206 +++++++++++++++++++++ prompts/dopamine-ux-05-workout-app.md | 230 ++++++++++++++++++++++++ 2 files changed, 436 insertions(+) create mode 100644 prompts/dopamine-ux-04-screen-locker.md create mode 100644 prompts/dopamine-ux-05-workout-app.md diff --git a/prompts/dopamine-ux-04-screen-locker.md b/prompts/dopamine-ux-04-screen-locker.md new file mode 100644 index 0000000..1e3f124 --- /dev/null +++ b/prompts/dopamine-ux-04-screen-locker.md @@ -0,0 +1,206 @@ +> **This file is a ready-to-use prompt.** Open this repo and say +> "do dopamine-ux-04-screen-locker". It is self-contained -- it needs no context from +> any other session. +> +> Canonical copy: `~/utils/dopamine-ux/04-screen-locker.md` (source of truth; this is a +> distributed copy). Running order and cross-prompt rules: `~/utils/dopamine-ux/00-INDEX.md`. +> Generated 2026-08-16 from a survey of this repo -- line numbers are accurate +> as of that date; **anchor on the symbol names, not the line numbers.** + +## ⛔ WHEN YOU FINISH THIS PROMPT + +Do **both**, in the same session that completes the work: + +1. **Delete this file** and commit that deletion together with the + implementation: + ```bash + git rm prompts/dopamine-ux-04-screen-locker.md + ``` + Do not leave a finished prompt lying in the repo -- a stale prompt is + indistinguishable from a pending one, and the next session will re-run it. + +2. **Log completion in the canonical index**, in `~/utils` (a *different* repo, + so it needs its own commit): + ```bash + cd ~/utils + # append to the "Completion log" table in dopamine-ux/00-INDEX.md: + # | 04-screen-locker.md | DONE | | | + git add dopamine-ux/00-INDEX.md + git commit -m "dopamine-ux: mark 04-screen-locker.md done" + git push + ``` + +If you finish only part of the prompt, do **neither** -- instead edit this file +to strike out what is done and state what remains, so the next session starts +from the true state. Partial completion must never be recorded as DONE. + +--- + +Surface the workout streak that screen-locker already tracks + +## findings (3, ranked by value over effort) + +1. **`snapshot.streak` is populated and no renderer reads it** — the cheapest + win in the whole project. Zero new state, zero conftest change, zero deps. +2. **Week-transition reward strings are discarded into `_logger.info`** — + fully-formed celebratory sentences that never reach a user. +3. **The success moment exists only on the lock screen**, for 1.5s. The status + window — the surface visited *voluntarily* — shows only enforcement. + +## what + +screen-locker computes a real workout streak and celebratory reward text, then +throws almost all of it away. Verified 2026-08-16: + +- `StatusSnapshot` (`screen_locker/_status_types.py` ~:88-101) carries populated + `streak`, `bonus_hours_this_week` and `early_bird_extended` fields, filled by + `_status_data.py` ~:210-214. **Grep confirms no renderer reads them.** +- `_extra_benefits.py` `process_week_transition()` (~:82, reward strings built at + ~:130-139) produces fully-formed celebratory sentences — and + `_startup_checks.py` ~:57-58 sends them to `_logger.info` and nowhere else. +- The one success moment that exists is on the **lock screen**: + `_unlock_view.py` `unlock_screen()` (~:25) paints "Great job! 💪" and + `🔥 {streak}-week streak`, for `_UNLOCK_DELAY_MS = 1500` before routing away. + +So the surface the user visits **voluntarily** — the status window, opened from +the tray icon — shows enforcement and budget depletion, and no evidence of +success at all. This is the cheapest high-value change in the whole project: +the data already exists, is already true, and needs no new state. + +## where + +Repo: `~/screen-locker`. + +Primary: +- `screen_locker/_status_sections.py` — the section renderers used by the status + window (`_section_*` functions). **177 lines; has headroom.** Add + `_section_streak` here. +- `screen_locker/status_view.py` — `StatusWindow`, `render()`. Call the new + section from here. ⚠️ **246 lines — only 4 left under the 250 cap.** Expect to + extract something, or add the call in a way that costs ~1 line. +- `screen_locker/_status_types.py` — `StatusSnapshot` (~:88-101), the fields you + are rendering. Read-only for this task. +- `screen_locker/_extra_benefits.py` (179 lines) — `process_week_transition()` + and its reward strings; `current_streak()` ~:154, + `weekly_shutdown_bonus_hours()` ~:159, `has_extended_early_bird()` ~:171. +- `screen_locker/_startup_checks.py` ~:57-58 — where the reward strings are + currently discarded into a log call. +- `screen_locker/_unlock_view.py` (72 lines, lots of headroom) — the existing + success moment, if you extend it. + +## must + +- **Render the already-populated fields.** Add `_section_streak` to + `_status_sections.py` showing the weekly streak, this week's bonus hours, and + the extended-early-bird state, and call it from `status_view.py`'s `render()`. + Zero new state, zero conftest change, zero new dependency. +- **Stop discarding the reward strings.** `process_week_transition()` returns + human-readable celebratory sentences that currently only reach the log. Surface + them where the user will actually see them — the status window is the obvious + home; a milestone is worth more than a silent counter increment. +- Match the existing positive-status styling rather than inventing one: + `_status_sections.py` ~:73-78 already renders `"{n} above the weekly minimum!"` + in success green, and uses `✓` marks at ~:36 and ~:57. +- Keep the streak **true**. It counts real ISO weeks with ≥5 counted workouts + (`_weekly_check.py`: `WEEKLY_WORKOUT_MINIMUM = 5`, `COUNTED_WORKOUT_TYPES = + {phone_verified, runnerup_verified, manual_workout}`). Do not round, pad, or + "encourage" the number. + +- must not: **make any reward reduce enforcement.** This is recorded twice in the + repo as a deliberate decision: + > `_startup_checks.py` ~:88-93 — "there is no banked 'skip a workout' credit — + > that mechanic works against the goal of maximizing weekly workouts, so it was + > removed in favor of a shutdown-time-only reward" + > `_extra_benefits.py` ~:5-8 — "This never reduces enforcement … it only grants + > extra comfort time on top of a floor you still have to earn each day." + + Every reward here is **display or comfort-time only**. Do not reintroduce a + banked skip, a streak freeze, a grace day, or any mechanic that lowers the + floor. A dopamine loop that lets you skip the workout defeats the app. +- must not: add **sound** to any enforcement or lock path. screen-locker is a + self-restriction tool; a chime on lock is punishment audio, and a chime on + unlock trains the wrong association. This repo stays silent. (Sound is + opt-out-on elsewhere in the fleet; here it is simply absent.) +- must not: add a write tool to the MCP server. `_mcp.py` ~:14-26 states it is + read-only by design. Adding `streak` to the existing `get_status` output is + fine; a new action tool violates a stated invariant. +- must not: add `# noqa` (banned repo-wide) or a silent `except` + (`scripts/check_silent_failures.py` — every handler re-raises or logs at + warning or above). +- must not: create a new on-disk state file unless genuinely unavoidable. If you + do, you **must** add it to `_ISOLATED_STATE` in + `screen_locker/tests/conftest.py` (~:134), listing *every* module that binds + the path at import time — a missed binding lets tests write to kuhy's real + state. Read the comment at ~:131-133 and the fixture docstring at ~:170-175. + +- optional: a daily streak alongside the weekly one. Note this **would** be new + state (the existing streak is consecutive-*weeks*), so it triggers the conftest + requirement above. diet-guard's consecutive-*day* streak + (`~/diet-guard/diet_guard/_daystatus.py` ~:144) is the precedent to mirror. + +## done + +1. Opening the status window (tray icon → click, or `screen-locker-status`) shows + the current streak, bonus hours and early-bird state — with the real values + from `extra_benefits_state.json`, not placeholders. +2. A week-transition milestone reaches the UI instead of only `_logger.info`. +3. `cd ~/screen-locker && python -m pytest` passes. +4. `pre-commit run --files ` is clean — including the 250-line cap + (`scripts/check_file_length.py`) and the silent-failure check. +5. `git diff` touches no enforcement logic: no change to lock decisions, weekly + minimums, or shutdown floors. + +## verify + +Desktop — this is a desktop Python app. + +``` +cd ~/screen-locker +python -m screen_locker --status # CLI: confirm the values it prints +screen-locker-status # the Tk window: confirm they render +``` + +Paste both outputs. The CLI already prints `streak`/`bonus_hours` (`_status.py` +~:138-141), so it is your ground truth: the window must show the **same numbers**. +If they disagree, the renderer is wrong, not the data. + +Current real state for comparison: `screen_locker/extra_benefits_state.json` and +`screen_locker/workout_log.json` (12 days of entries, 2026-07-12 → 2026-08-15). + +## read first + +- `screen_locker/_status_sections.py` — every existing `_section_*`. Match their + signature and style; note the success-green precedent at ~:73-78. +- `screen_locker/_status_types.py` ~:88-101 — the three fields you are rendering. +- `screen_locker/_extra_benefits.py` — especially the module docstring ~:5-8 + (the enforcement invariant) and the reward strings ~:130-139. +- `screen_locker/_unlock_view.py` — the existing celebration, for tone. +- `screen_locker/status_view.py` — check its line count **before** editing. +- `~/screen-locker/CLAUDE.md` — repo rules (no silent failures, ruff `select=ALL`). + +## context you would otherwise rediscover + +- **`workout_log.json` is the single source of truth**, written only through + `_log_mixin.py` `write_signed_entry()` (~:82), HMAC-signed via + `gatelock.log_integrity`, deduped on `workout_id`. The Flutter app converges on + it via `_manual_push.py`. There is no second history — a streak computed from + it is authoritative. +- All persistence is **JSON files, no sqlite**; paths declared in + `screen_locker/_constants.py`. The streak store is + `screen_locker/extra_benefits_state.json` (`consecutive_5plus_weeks`, + `last_processed_iso_week`, `weekly_shutdown_bonus_hours`, + `extended_early_bird_iso_weeks`). +- Thresholds live in `_extra_benefits.py`: `_MILESTONE_INTERVAL = 4` (4-week + milestone), `_BONUS_THRESHOLD = 5` (workouts per week). +- **There is no config system** — no argparse, no env vars, no config file. Every + tunable is a module-level constant. An opt-out toggle has no existing home; + for a self-restriction tool, requiring a code edit to disable is arguably + correct, so prefer a constant in `_constants.py` over new state. +- `~/gatelock` is **not** a repo — it is `~/utils/gatelock/gatelock/`, consumed as + a pinned git dep, and it holds no workout state. +- Manual workouts were **not** removed: `_manual_workout.py` implements a + rate-limited, evidence-gated subsystem (budget 2 per 7 days, 10 per 30). +- Three UI surfaces exist: the Tk status window (`status_view.py`), a GTK tray + icon at `~/.config/i3/scripts/screen_locker_tray.py` (outside the repo, shells + out to the CLI, refreshes every 60s), and the lock screen itself. No i3blocks + integration exists despite `format_summary_line` being described as one. diff --git a/prompts/dopamine-ux-05-workout-app.md b/prompts/dopamine-ux-05-workout-app.md new file mode 100644 index 0000000..f7ab68b --- /dev/null +++ b/prompts/dopamine-ux-05-workout-app.md @@ -0,0 +1,230 @@ +> **This file is a ready-to-use prompt.** Open this repo and say +> "do dopamine-ux-05-workout-app". It is self-contained -- it needs no context from +> any other session. +> +> Canonical copy: `~/utils/dopamine-ux/05-workout-app.md` (source of truth; this is a +> distributed copy). Running order and cross-prompt rules: `~/utils/dopamine-ux/00-INDEX.md`. +> Generated 2026-08-16 from a survey of this repo -- line numbers are accurate +> as of that date; **anchor on the symbol names, not the line numbers.** + +## ⛔ WHEN YOU FINISH THIS PROMPT + +Do **both**, in the same session that completes the work: + +1. **Delete this file** and commit that deletion together with the + implementation: + ```bash + git rm prompts/dopamine-ux-05-workout-app.md + ``` + Do not leave a finished prompt lying in the repo -- a stale prompt is + indistinguishable from a pending one, and the next session will re-run it. + +2. **Log completion in the canonical index**, in `~/utils` (a *different* repo, + so it needs its own commit): + ```bash + cd ~/utils + # append to the "Completion log" table in dopamine-ux/00-INDEX.md: + # | 05-workout-app.md | DONE | | | + git add dopamine-ux/00-INDEX.md + git commit -m "dopamine-ux: mark 05-workout-app.md done" + git push + ``` + +If you finish only part of the prompt, do **neither** -- instead edit this file +to strike out what is done and state what remains, so the next session starts +from the true state. Partial completion must never be recorded as DONE. + +--- + +Close the feedback asymmetry in workout_app + +## findings (4, ranked by value over effort) + +1. **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. +2. **Set completion has no haptic** — the app's most repeated action, silent. +3. **The fleet's only animation is an inline magic number** — + `Duration(milliseconds: 200)` in `exercise_tile_rows.dart`. +4. **No sound opt-out exists** — the SQLite settings table is ready, but no + public accessor and no `SwitchListTile` anywhere. 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()` in + `lib/screens/workout_screen_session.dart` (~:128-145) plays + `assets/sounds/break_end.mp3` and fires `Vibration.vibrate(duration: 800)`. +- **Finishing an entire workout** gets nothing. `_persistFinishedWorkout()` in + `lib/screens/workout_screen_finish.dart` (~:99-108) ends by showing + `WorkoutSummaryDialog` with no sound, no haptic, no animation. +- **Completing a set** — the core repeated action of the whole app — gets no + haptic either. The `GestureDetector` at `lib/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-completion `GestureDetector` ~:145, + the `AnimatedContainer` ~: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 `.catchError` only + `debugPrint`s (~:131-137). +- `lib/screens/workout_screen.dart` — `_audio = AudioPlayer()` ~:78, `vibration` + import ~: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`/`failStreak` on `ExerciseState` + (`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`. + **No `SwitchListTile` exists** — 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), accessors `lib/services/storage_service.dart` ~:122-137 + (`_getSetting`/`_setSetting`, both **private** — add a public typed wrapper like + the existing `setLastWorkoutType`). Already backed up/restored via + `lib/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 at + `exercise_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 into `lib/ui/theme.dart`. +- Animate the streak/stat numbers on the history screen to their new values + instead of snapping. `_ChartPainter.shouldRepaint` currently 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. +- `VIBRATE` is already granted (`android/app/src/main/AndroidManifest.xml` ~:10) + and `vibration: ^3.1.0` is already a dependency. Prefer `HapticFeedback` from + `flutter/services.dart` for short UI taps and reserve the `vibration` package + 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 at `pubspec.yaml` ~:52-53 — **this is the fleet's only `assets:` + block**, so you are adding a file, not the block). +- Sound ships **on**, with an opt-out. Add a public + `getSoundEnabled()`/`setSoundEnabled(bool)` on `StorageService` wrapping + `_getSetting`/`_setSetting` (store `'true'`/`'false'`), then a settings section + with a `SwitchListTile`. +- 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`/`failStreak` count 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` → + `debugPrint` at ~:131-137 is the local precedent; do not make it quieter. +- must not: break the **100% coverage gate** — new public methods on + `StorageService` need tests. See the comments at + `settings_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 + +1. Completing a set produces an immediate haptic on the phone. +2. Finishing a workout produces a haptic and a visible dialog arrival; step 2 + adds a sound. +3. No inline animation duration remains in `exercise_tile_rows.dart`. +4. `cd ~/screen-locker/stronglift_replacement/workout_app && flutter analyze` clean. +5. `flutter test` passes **and coverage stays at 100%**. +6. With OS "remove animations" on, the app works and durations are zero. +7. 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 + only `AnimatedContainer`. +- `lib/services/storage_service.dart` ~:122-137 — `_getSetting`/`_setSetting`, and + `setLastWorkoutType` as the public-wrapper pattern. +- `lib/screens/settings_screen_sections.dart` ~:1-11 — the coverage/line-cap + comments, and `_SyncSection` as 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_system` dependency**; `lib/ui/theme.dart` is a + hand-transcription of the shared tokens. Motion tokens must be transcribed + there, matching the existing convention. Do not add the dependency here. +- `shared_preferences` **is** a dependency but is used only for the device id + (`lib/services/sync_device_id.dart` ~:37-38). Settings go in SQLite. +- Non-motion `Duration`s 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.json` as 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 runs + `select = ["ALL"]` on the Python side.