170 Commits
Author SHA1 Message Date
rzen d24d0c3317 release.sh: add mac/all modes for the Workouts Mac target 2026-07-16 20:48:54 -04:00
rzen 04ab7ed005 Move the Mac toolbar buttons to the window toolbar
Sidebar-column toolbar items are confined to the sidebar's narrow
section of the unified toolbar and collapse into overflow; attaching
New Routine and Starter Gallery to the split view gives them the full
trailing section.
2026-07-16 20:44:07 -04:00
rzen 037753832c Never block reconcile on per-file downloads; request evicted files up front
On a first attach (new Mac, reinstall) the whole tree is evicted and the
bounded per-file wait (~30s, serial) left the UI empty for hours. Reconcile
now fires all download requests immediately, sweeps the files that are
already current, and parks evicted paths as unreadable (keeping their cache
entities) — the metadata observer imports each file the moment bird
materializes it.
2026-07-16 20:29:37 -04:00
rzen 373f812968 Adjust rest time from the watch with the same 1-second stepper
A gear on the watch root opens a small Settings sheet whose stepper
edits the shared restSeconds default (10-180s, 1s steps). Edits ride a
new settingsUpdate message to the phone - debounced per stepper burst,
falling back to transferUserInfo when unreachable - which clamps the
value, writes the shared default, and re-echoes it to every device
through the application context. While an edit is pending on the watch,
an in-flight context's stale rest value is skipped so it can't yank the
stepper back mid-edit.
2026-07-16 20:18:47 -04:00
rzen 47a49cc356 Stagger build numbers by platform: iOS/watch even, macOS odd
Both platforms share one App Store Connect build-number space (same
bundle ID, universal purchase), so the raw commit count would collide.
The vendored script partitions on PLATFORM_NAME; watchOS rides with iOS
because the embedded watch app's CFBundleVersion must match its host.
2026-07-16 20:14:23 -04:00
rzen 62ca446136 Adjust rest time with a 1-second stepper instead of the wheel
The wheel row lands back on a Stepper (shared SecondsStepperRow), but
keeps the fine 1-second step; holding a stepper button auto-repeats
with acceleration, so the 10-180s range still traverses quickly.
2026-07-16 20:10:30 -04:00
rzen cbdf02bca7 Add the macOS app: a library-management companion (routines, schedules, exercise browser)
A new 'Workouts Mac' target (same bundle ID as iOS, universal purchase)
with a NavigationSplitView shell over the shared data/sync layers:
routine management with starter gallery and seed-fork follow, schedule
editing (reminders stay iPhone-scheduled), and a browse-only exercise
library with the animated figures and reference guides.

Multi-writer stance: the Mac writes only routine and schedule documents
(whole-document last-writer-wins) and never workout documents, whose
per-log merge exists only on the watch ingest path. The Mac reconciles
on window activation (throttled) since iCloud syncs while the app is
closed, and flushes pending writes on deactivation.
2026-07-16 19:55:06 -04:00
rzen f6c5bc911f Share schedule-editing and starter-seed-state helpers across platforms 2026-07-16 19:54:57 -04:00
rzen 9968cb2056 Guard reconcile reentrancy and deleted-routine duplication; cache exercise info parsing 2026-07-16 19:54:57 -04:00
rzen fe4a7a3ca1 Extract the exercise info view and ready the figure palette and app icon for macOS 2026-07-16 19:54:56 -04:00
rzen 9741b7d834 Pick rest time on a 1-second wheel
Replaces the 5-second Steppers in Settings and the routine editor's
custom-rest row with a shared SecondsWheelRow: the row shows the value
and tapping it expands an inline wheel (10-180s, 1s steps) beneath.
2026-07-16 19:44:22 -04:00
rzen aa23d7baa7 Add a live status panel between the run screen's halves
Replaces the small target-HR pill on the iPhone run screen with a
glanceable big-digit strip between the timer flow and the figure —
heart rate (band-tinted with the cue arrow when the run carries a
target), HR zone, active calories, and the workout stopwatch. A
horizontal band in portrait, a vertical column in landscape (the
inverse of the half-and-half split, so it always sits between them).

The watch now rides the running calorie total and the HR zone (1-5,
computed watch-side where max HR is known) along with each live HR
sample; absent keys keep older builds wire-compatible both ways.
LiveRunState holds all three under the shared staleness expiry. The
screenshot rig seeds believable values so the run capture shows the
panel populated.
2026-07-16 18:54:39 -04:00
rzen a0f14f46e1 Document the Library tab in the changelog and README
Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-15 11:15:26 -04:00
rzen 52f58cd79c Surface routine usage, starter-copy notices, and a grouped exercise picker
Routine detail gains a read-time Usage section (last trained, completed
workout count, linked schedules) resolved through the clone redirect, and
both it and the edit sheet now explain that editing a starter saves your
own copy. The add-exercise picker adopts the same curated category
sections and name/category/muscle search as the exercise library.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-15 11:14:24 -04:00
rzen b6046bd0a9 Add a starter routine gallery with per-seed restore
The Library tab's routines pane gains a "Browse Starter Routines" entry
opening a gallery of every bundled seed — kept or deleted — each showing
Added / name-taken / restorable state, a read-only plan preview, and a
one-tap Add that lifts the seed's delete veto. A "Re-add All Missing
Starters" fallback replaces the all-or-nothing Settings button, which is
removed. The delete confirmation now also warns how many scheduled days
would be left behind on the Today board.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-15 11:14:14 -04:00
rzen 586804f771 Add the Library tab: reorderable routines pane and searchable grouped exercises pane
A fourth tab promotes routine and exercise management out of Settings.
LibraryView switches between two segments on one navigation stack. The
routines pane (RoutineListView reborn as RoutinesLibraryView) sorts by
the user's order, gains drag-to-reorder writing only changed orders,
swipe actions for duplicate/edit/delete, a Starter badge on bundled
seeds, and a last-trained caption. The exercises pane groups the library
into curated category sections searchable by name, category, or muscle.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-15 10:58:57 -04:00
rzen 16b61946d5 Treat order as presentation in seed logic; add per-seed restore and routine duplication
Drag-to-reorder writes Routine.order through the document, which used to
read as a content edit: isPristine would fork a starter for a mere reorder,
and reconcile's semantic compare would clobber a reordered seed file back
to bundle order. Both now normalize order (and updatedAt) away — a fixed-
ULID file still never holds user content; ordering is bookkeeping.

SyncEngine gains restoreSeed(id:) — the per-seed analog of the bulk
restore, sharing one restoreSeedIfEligible core — and duplicate(routine:),
which copies any routine (starter or not) to a fresh ULID with fresh
exercise ids, a unique "… Copy" name, and last position in the list.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-15 10:45:02 -04:00
rzen 36dad21233 Add pure helpers for routine ordering, copy naming, and a grouped exercise catalog
Groundwork for the Library tab: RoutineOrdering.changedOrders turns an
.onMove gesture into the minimal set of order writes (normalizing legacy
colliding orders on first drag), RoutineNaming.uniqueName picks the next
free "X Copy" name for duplication, and ExerciseCatalog groups the bundled
exercise library into curated category sections with name/category/target
search. Plus ExerciseDocument.planSummary for read-only document rendering.
All covered by unit tests, including a motion/info name-parity assertion.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-15 10:38:43 -04:00
rzen ce4c581bd0 Remove the duplicate exercise list screen and dead reorder stubs
ExerciseListView was a near-duplicate of RoutineDetailView's exercise
management, reachable only from RoutineAddEditView's "Exercises" row —
drop both, leaving RoutineDetailView as the single exercise manager.
OrderableItem and SortableForEach were self-declared dead stubs.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-15 10:38:35 -04:00
rzen c39eeda9df Port the exercise run screen's true resume to the watch
Re-entering an in-progress exercise on the watch now computes its landing
(page + wall-clock anchor) from the log's durable timestamps in init —
mid-set resumes the stopwatch from startedAt, mid-rest lands on the rest
page with the countdown continuing and auto-advancing at the true
boundary, and a spent rest anchors the next set at the rest's computed
end. Timestamps are clamped to now against peer clock skew; legacy logs
without them keep the old first-unfinished-set behavior. broadcastLive
forwards the resume anchor so a mirroring phone's timer lines up.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-14 22:01:49 -04:00
rzen 4cd6e4181c Show both identities in a renamed exercise's headline and repair legacy figures
The run-screen headline now reads "Library · Name" for a renamed exercise,
keeping its origin visible next to the custom name (the separate subtitle
line is gone). Logs minted before the per-log libraryName snapshot existed
carry none, which cost a renamed exercise its figure, guide, and spoken
cues — the screen now resolves the library identity through the workout's
routine (same-named exercise, display-only) as a fallback, so those older
workouts get their animated figure back, including on the Completed page.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-14 21:30:00 -04:00
rzen e8b1cb800d Give the exercise run screen its own chrome, rolling digits, and a true resume
The run screen now hides the navigation bar in both orientations and draws
its own header — back chevron top-left, exercise name (plus library name
when renamed) in large type where the toolbar was, previewing the next
exercise during a between-exercise rest. Work and rest counters are pinned
to the exact vertical center of the timer half via equal flexible bands,
with the adjust pill moved to an overlay so complications can't shift them.
Counter digits are now state-driven and roll through numericText
transitions (work count-up, rest countdown, and the Done button count).

Reopening an in-progress exercise now resumes its timer from the durable
timestamps instead of restarting at zero: no sets done anchors the
stopwatch to startedAt; mid-rest lands back on the rest page with the
countdown still running off its true window; past the window the next
set's stopwatch counts from the rest's computed end. The resume page is
computed at init because the paged TabView must initialize on it — a
post-layout backward jump wedges the pager, which then ignores the next
animated programmatic advance.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-14 20:56:18 -04:00
rzen df5c77eee1 Let routines rename exercises and repeat them for interval segments
An exercise's name is now a per-routine display name: a new optional
libraryName on ExerciseDocument (snapshotted onto WorkoutLogDocument at
plan time) keeps the link to the bundled library exercise, and every
figure/info/cue lookup resolves libraryName ?? name. Deliberately not
schema-bumped — an older app dropping the key only strands the
figure link, same rationale as activityType. Cache schema bumped to 9
for the new columns.

The picker no longer filters out exercises already in the routine
(an "×N" badge marks them instead), exercise rows gain a leading
Duplicate swipe that clones an entry in place, and the edit sheet gets
a Name field with the library exercise shown read-only above it.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
2026-07-12 18:38:14 -04:00
rzen d04526826c Add a Running exercise with an animated stride figure
Stance and flight keyframes with single-frame foot pins for the strikes,
forward lean, opposite-arm drive, and daylight under both feet in flight —
distinct from Cardio's high-knee march, which stays as is.

Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
2026-07-12 00:37:23 -04:00
rzen 79e75a9127 Fix figure IK snapping and gate the library on a fail-hard motion checker
Three solver defects made limbs teleport, twist, or windmill: write-back
angles wrapped at ±180 and lerped the long way around; branch flips landed
on configurations the anatomical write-back cannot represent, silently
pulling pinned extremities off their pins; and the degenerate straight-limb
bend plane fell back to the camera axis instead of the anatomical anterior.
solve_limb now verifies each branch reproduces the solved end before
accepting it, resolve unwraps written-back angles toward the pose they
replace, and the degenerate plane comes from the parent's anterior axis.

render.py --check replays every exercise's full tween loop and fails hard
on six invariants (pin fidelity, continuity, wraps, authored-vs-resolved
drift, ground penetration, resolved ROM); --export refuses to ship a
failing exercise. All 66 motions re-authored or retouched to pass: honest
authored angles where pins used to override them silently, grounded feet
on the seated machines, a vertical bench-press bar path, straight-armed
child's pose, a butterfly stretch seated on the mat, and FK arms where
pins forced impossible reaches. MotionSolver.swift mirrors the solver
changes line for line, held by regenerated fixtures.

Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
2026-07-12 00:37:23 -04:00
rzen 400601283e Add a debug figure-review pager under Settings > Developer
Swipe through every bundled exercise rig, toggling between the looping
figure and its authored reference page, to QA the library on device.

Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
2026-07-12 00:37:05 -04:00
rzen 530021c2de Give the Progress tab a rotating daily title greeting
Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
2026-07-12 00:37:05 -04:00
rzen c24bc9cfeb File ad hoc workouts under their routine's goal on the Today board
An ad hoc workout was only launched ad hoc — any schedule referencing the
same routine (through the clone redirect) still says what the user trains
it for. Infer the goal from those schedules, tie-breaking by the user's
goal order; only truly unassigned workouts stay in Unassigned.

Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
2026-07-12 00:37:05 -04:00
rzen 7b6c39d8f0 Guard every persisted read of retained workout models with isLive
A remotely deleted @Model traps on any persisted-property read, including
the observed expression of .onChange, which SwiftUI evaluates on every
body pass. Guard those expressions, the onAppear takeover read, and the
summary sheet's document mapping.

Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
2026-07-12 00:37:05 -04:00
rzen 8854ad59d5 Add per-routine target heart rate with live in-run indicator
Endurance routines (HIIT, cardio, cycling) can carry an optional target bpm
(RoutineDocument.targetHeartRate, not schema-bumped — same preference-field
rationale as restSeconds/autoAdvance), snapshotted onto the WorkoutDocument at
plan time like the other pacing fields.

During a run, the watch streams its live HR sample to the phone over a new
best-effort liveHeartRate message — deliberately outside the LiveProgress
machinery (no version bump, no staging/retry; a gauge, not a record), throttled
to changed-bpm-or-10s in the watch bridge. LiveRunState holds the sample with a
30s staleness auto-clear so a dead stream never shows a frozen number.

Both run screens show the reading only when the run carries a target: the
phone's ExerciseProgressView as a top pill, the watch's in the top-trailing
toolbar slot, each tinted by a shared ±5 bpm HeartRateBand with an arrow cue to
push harder (low) or ease off (high) — e.g. dialing in a treadmill incline to
hold a steady effort.

Claude-Session: https://claude.ai/code/session_01Y7ZhkCYWNiTSAFhFCGnJ8n
2026-07-11 19:51:44 -04:00
rzen b06a44eb40 Hide the tab bar when the exercise run screen is in landscape
The landscape side-by-side split (paged flow | form-guide figure) is already
starved for height; keying tab-bar visibility off the same compact-vertical
size class that drives the split gives the run the full screen. Applied across
all three body branches (run flow, Completed, Skipped).

Claude-Session: https://claude.ai/code/session_01Y7ZhkCYWNiTSAFhFCGnJ8n
2026-07-11 19:49:55 -04:00
rzen b1a5d25b61 Give achievements tappable detail pages with how-to-earn and why-it-matters copy 2026-07-11 13:40:36 -04:00
rzen e509d0e7c5 Keep schedules attached to routines across seed clone-on-edit forks
Editing a starter-seed routine forks it to a fresh ULID, but only workouts
were durably repointed — schedules kept the dead seed id, and after a
relaunch (empty in-memory redirect map) the Today board and edit form saw
the routine as gone.

- cloneSeedOnEdit now repoints schedules too (routineID + routineName
  track the clone; a schedule's name is a live-pointer fallback, unlike a
  workout's frozen run-as name)
- A launch-time repair pass (pure ScheduleRepairPlanner + tests) heals
  already-broken references by re-attaching a dead-pointer schedule to
  the unique live routine matching its remembered name
2026-07-11 13:02:55 -04:00
rzen 76150070f8 Show the selected routine as a symbol-and-color preview in the form
The Routine row now renders the selection like the picker list's rows
(tinted symbol + name); with nothing selected it reads "Please select"
in secondary style.
2026-07-11 12:50:21 -04:00
rzen 1a1147b612 Resolve the seed redirect when editing a schedule's routine
A schedule made against a starter-seed routine keeps the retired seed id
after the routine is edited (clone-on-edit). The edit form matched that id
directly, so the routine read as unselected and Save stayed disabled; it
now resolves through sync.currentRoutineID like the Today board does, and
the routine picker's checkmark compares against the resolved id.
2026-07-11 12:40:18 -04:00
rzen 5ec5de9295 Add reminder notifications to schedules
- ScheduleDocument/Schedule gain optional reminderMinutes (minutes from
  midnight; decode-compatible, no schema bump)
- New Workout form gets a Reminder section (toggle + time picker, hidden
  for Now; footer warns when notifications are denied)
- ReminderPlanner (pure, tested) derives notification triggers: daily
  repeating, per-weekday repeating for fixed days, one-shot for once
- ReminderScheduler resyncs pending requests from the cache on every
  change, multiplexed onto onCacheChanged after the watch push; asks
  notification permission only once a reminder actually exists
2026-07-11 12:32:08 -04:00
rzen df07e0a043 Polish the New Workout form: segmented When on top, pushed routine list
- When section moves to the top as a segmented control (3 or 4 segments
  depending on whether Now is offered)
- Routine selection is a pushed list with each routine's symbol and color,
  checkmark on the current pick, select-and-pop
- Goal "None" reserves the symbol slot so all goal labels align
2026-07-11 11:39:29 -04:00
rzen 7cb2d6da26 Show only the day's docket on the Today board and add ad hoc Now workouts
- Due-filter schedules: daily always, fixed days by weekday, one-offs on
  their date; rest days get an empty state
- Workouts with no due schedule row (ad hoc or off-day starts) now render
  as their own board rows
- The + sheet is now "New Workout" with a "When" picker; "Now" (offered
  when adding on today) skips the schedule and starts the workout directly
- Remove the times-per-week scheduling mode everywhere (enum, document,
  entity, mappers, planner, seeds, tests)
2026-07-11 11:09:11 -04:00
rzen 9dc9283b4c Record the persistence-migration proposal (documents -> pure database)
Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
2026-07-11 08:20:39 -04:00
rzen 20d7f00843 Fix the day picker's workout-day dots missing on most days
The decorated-days set was built from dateComponents([.year, .month,
.day], from:) output, which also sets isLeapMonth = false; the day
picker probes it with bare y/m/d components (isLeapMonth nil). The two
compare == but hash differently, so Set.contains missed essentially
every member and dots appeared only on chance hash collisions. Build
the set from bare y/m/d components matching the probe's shape.

Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
2026-07-11 08:20:39 -04:00
rzen f8014e62ac Add day navigation to the Today board toolbar
Back/forward chevrons step the board a day at a time around the
existing calendar button, and a Today button — shown only when the
board is off today — jumps back. Also add the missing changelog
entries for the Today board itself.

Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
2026-07-11 08:20:39 -04:00
rzen 9a832ad960 Rework the spoken count-in and count timed sets out
The count-in now speaks the exercise name at 5s left, leaves the 4s
beat silent so a long name can finish, then counts down — "in three,
two, one, Go!" (it previously counted up from 4s). Timed work sets
get a mirrored count-out: "<Exercise> ends." at 5s, the same buffer
and countdown, and silence at zero where the rest buzz marks the
boundary; the final "one" releases the audio duck.

Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
2026-07-11 07:53:11 -04:00
rzen 6e440317c4 Restructure into a three-tab app with Progress, goals, and Meditation
The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView
becomes a Today / Progress / Settings TabView, "Routine" replaces
"Split" in every user-facing string and view name (code-level types
keep their names), and workout starting moves to shared
WorkoutStarter / StartedWorkoutNavigator plumbing.

- New Progress tab: weekly goal streaks, workout trends, per-exercise
  weight progression, achievements, and the full history list
  (WorkoutLogsView -> WorkoutHistoryView).
- Goals: stable categories workouts roll up to, managed from Settings.
- New Meditation exercise + starter routine; timed sits record to
  Apple Health as Mind & Body sessions.

Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
2026-07-11 07:53:01 -04:00
rzen 5c201289fb Survive deleted-and-saved @Models on the started-run navigation path
TestFlight 2.3 (125) "crashed when watch ended an exercise": the
isDeleted guard from 85e1582 only covers the delete→save window. Once
the deletion is saved the model unregisters — isDeleted reads false
again, modelContext goes nil, and any persisted-property read still
traps (_InitialBackingData.getValue). StartedWorkoutNavigator retained
the run's @Model in @State for the whole workout, so an observer
remove/re-add churn (e.g. iCloud reachability flapping) invalidated it
underneath the pushed screen, and the watch's completion push triggered
the rebuild that read it.

Two layers: the fromLive/SplitDetailView guards now also require a
non-nil modelContext, and StartedWorkoutNavigator pushes a plain id
route, re-fetching the entity fresh on every destination build — a
re-imported run resolves to its live instance; a gone run shows a
placeholder instead of trapping.

Claude-Session: https://claude.ai/code/session_01PVNBVKp5bcq52X722uMjwT
2026-07-10 11:32:52 -04:00
rzen 372b5dfb4d Record L4 device result and severing alternatives in BULLETPROOFING.md
Variant A refuted on device 2026-07-10 (phone kept its higher count; the
win was timing, not a structural guard — the merge remains pure
newest-modTime-wins). Step 2 now lists ranked link-severing methods,
since phone-side Airplane Mode provably doesn't cut the watch link.

Claude-Session: https://claude.ai/code/session_01PVNBVKp5bcq52X722uMjwT
2026-07-10 11:32:42 -04:00
rzen 85e15823a3 Guard NavigationLink destination inits against deleted @Models
A closure-based NavigationLink builds its destination view eagerly for
every row on every parent-list body evaluation — including the view-graph
update SwiftUI runs the instant SyncEngine.delete does context.delete +
save. Mapping the row's just-deleted @Model to a document there reads a
persisted property on a dead model, which traps in SwiftData
(_InitialBackingData.getValue -> assertionFailure). This is the "Crashed
when deleted a workout" TestFlight report on 2.3 (124).

Add WorkoutDocument(fromLive:) (nil when isDeleted) + a .deletedPlaceholder
and route every destination-init / updatedAt-absorb map through it:
phone WorkoutLogListView + SplitDetailView (read split.id) and watch
WorkoutLogListView. Fixes the whole crash class, not just the report.

Claude-Session: https://claude.ai/code/session_01BQcEWmAPA78338QuEwRkAh
2026-07-10 10:47:10 -04:00
rzen 8b20d7ad08 Preview the next exercise and count into each set during rests
The run flow's between-exercise rest now previews the next exercise — its
figure and a large "Coming up" name — and the hands-free narration gains a
per-second count-in ("<Exercise>, in 1, 2, 3, GO!") spoken before every set,
plus a "Coming up" announcement as a between-exercise rest begins. A new
Settings > Narration picker chooses the count-in cues, the setup/form read,
or both.

Also fixes spoken cues going silent after the first exercise in a flow split:
the stop-on-teardown moved from the per-exercise view (rebuilt on every
hand-off) to the run host, which stays mounted for the whole run. The audio
session now holds its duck across the per-second count so background music
doesn't pulse between words.

Claude-Session: https://claude.ai/code/session_01BQcEWmAPA78338QuEwRkAh
2026-07-10 09:38:45 -04:00
rzen bce2bf7539 Record the L4 device test protocol in BULLETPROOFING.md
The rollback repro (watch-side airplane mode, phone-ahead/watch-behind
divergence, queued transferUserInfo delivery), the ready-made L4 Merge
Test split in the iCloud container, confirm/refute criteria, and the
deferral caveats (monotonic clamp would break the deliberate
swipe-back-to-Ready reset).

Claude-Session: https://claude.ai/code/session_01PVNBVKp5bcq52X722uMjwT
2026-07-10 06:53:13 -04:00
rzen ccd5194c6c Surface phone/watch schema mismatch on the watch; log dropped pulls
The decode-failure freeze (BULLETPROOFING.md L3) is production-reachable
after all - a phone app can update days before the watch app
auto-updates, and every push in between fails to decode, silently
freezing the watch at its last good sync. The bridge now tracks
schemaMismatch (set on a failed decode, cleared on the next good apply)
and ActiveWorkoutGateView shows an "Update both apps to resume sync"
banner while it's set.

Also give requestSync an error handler that logs the dropped pull (L2;
deliberately no retry - the activation/reachability edges re-pull), and
sync DEVICE-COMMUNICATION.md to the post-bulletproofing reality (T1 as
optimization, session recovery, degraded pushes, surfaced mismatch).

Claude-Session: https://claude.ai/code/session_01PVNBVKp5bcq52X722uMjwT
2026-07-09 23:12:39 -04:00
rzen 62a2882f0e Dismiss stale exercise flows on remote resolution; unfreeze the cover
Two stale-UI traps in the run flow (BULLETPROOFING.md M2, M3):

A log resolved remotely (completed or skipped on the other device) left
the open exercise screen live - its next recorded set wrote the log
back to in-progress, resurrecting it through the per-log merge. Both
platforms' ExerciseProgressView now observe the log's status and
dismiss on a remote terminal flip (a locallyResolved flag exempts the
screen's own Done / flow hand-off). The watch also gains the phone's
startsSkipped terminal page, so opening a skipped exercise shows a
static badge instead of a live flow.

The live-mirror cover ran the flow engine with no onAdvance host, so an
auto-advance split's terminal between-exercise rest completed the
exercise then froze at 0:00. With no hand-off host it now dismisses
instead; the driver's next-exercise frame re-presents the cover.

Claude-Session: https://claude.ai/code/session_01PVNBVKp5bcq52X722uMjwT
2026-07-09 23:09:50 -04:00