Dev Diary

Make the first workout ready to use

INCREMNT is an iOS strength-training app that turns a goal, schedule, and available equipment into a workout plan. This week I tightened the handoff from that plan to a first workout that fits real life, starts honestly when no load history exists, and can travel across the phone and a Garmin companion without losing ownership.

  • Onboarding now puts the first workout at the centre of the plan reveal, with its duration, exercise count, and a clear “Start here” handoff.
  • Programme generation now supports two full-body days and a real 30-minute session target, while preserving older compact-plan data.
  • Exercises with no benchmark load now stay explicitly uncalibrated until the athlete chooses a weight, instead of receiving a made-up estimate.
  • Built the first Garmin workout app and companion protocol with persisted snapshots, queued operations, receipts, and conflict boundaries.

Product: the plan has to meet the athlete at the first set

INCREMNT asks an athlete about the things that determine whether a plan will survive contact with a week: goal, lifting experience, schedule, equipment, and how much time a session can take. The important product moment comes after those answers. The athlete needs to know what to do first, and needs to believe the first set is theirs—not a generic demo of what the app might do later.

The onboarding reveal now makes that handoff explicit. It leads with the first day of the generated programme, labels it START HERE, shows the approximate duration and exercise count, and says it is ready when the athlete continues. The supporting metrics are quieter in this context. Training details still exist, but the first action gets the visual priority. Goal proof also uses a different visual language for strength, hypertrophy, and general fitness rather than forcing every goal through a single body-composition curve. The point is not more decoration. It’s to make the promise legible: this is the workout your answers produced.

That promise has to fit the week the athlete actually has. A two-day schedule now produces two full-body sessions, with legs, a push, and a pull represented across both days and squat and hinge work separated across the week. A 30-minute choice is a genuine generation target, not a relabelled 40-minute preset. The compact policy changes its set capacity and estimated duration, and the old compact30 value still decodes as the legacy compact option. That compatibility matters because a new preference should not make an existing programme unreadable.

The first set also needs to be honest. If an athlete skipped a benchmark, INCREMNT now records that absence as needs calibration. It keeps the exercise structure and reps, but it does not quietly fill in a strength estimate or copy a template weight into a replacement exercise. The Logger shows an inline instruction to choose a comfortable working weight, refuses to complete a loaded set at zero, and still treats a true bodyweight movement correctly. The first completed working set becomes the evidence for the next recommendation.

This is a small boundary with a large effect. A guessed number can look polished while making the first workout feel unsafe or strangely specific. An explicit blank tells the athlete exactly what decision belongs to them, and gives the progression system a clean fact to build on.

Building INCREMNT: carry intent across boundaries without inventing certainty

The technical work underneath this story was about keeping one meaning intact as it moves through several representations: onboarding answers, a generated programme, a Logger exercise, and—now—a watch-owned workout. The reusable lesson is to model the exceptional state instead of hiding it in a default.

For programme generation, that meant adding short30 and a two-day ProgramShape, then threading the choice through the Core policy, iOS models, previews, hosted creation contracts, and decoding tests. The two-day shape is deliberately concrete: each day has a squat or hinge, a horizontal push, a pull, core work, and an optional isolation slot before the generator trims it to the compact target. Tests assert the shape for every training goal and verify the legacy decoding path. The shared policy file is generated data, but the Swift policy and contract tests remain the place where the behaviour is explained.

Calibration required a different kind of explicitness. GeneratedPrescriptionSourceKind.needsCalibration is not another flavour of estimate; it means there is no load evidence yet. The materializer encodes that state, the Logger completion guard understands the difference between an unset loaded exercise and a legitimate zero-load bodyweight exercise, and exercise swaps preserve the state instead of importing a convenient template weight. Codable coverage keeps the marker round-trippable for older programme data. The focused tests cover the uncomfortable paths: an uncalibrated swap, a zero-weight loaded set, a chosen positive weight, and a bodyweight movement.

The Garmin work extends the same ownership rule to a second device. The phone remains authoritative for plans, access, history, and progression. The native Garmin app receives a versioned, chunked snapshot and keeps an active workout plus an outbox in persistent storage. Every watch operation carries an immutable ID, binding, workout identity, sequence, and base revision. The phone sends a receipt only after the domain save; conflicts and rejections stay visible instead of being retried forever or silently dropped. A restart can request a fresh snapshot, and a partial transfer is discarded unless its chunks agree on identity, revision, counts, and order.

That design is intentionally stricter than “the watch sent a message.” A delivery callback is not a committed set. A finish receipt means the phone completed the workout, not merely that a notification was posted. Only one recorder owns a workout, so a Garmin session cannot accidentally start a second phone HealthKit lifecycle. We also keep canonical weights in kilograms across the wire and convert only for watch presentation. Those constraints make offline logging and retries boring in the best sense: the same operation can be replayed, a stale edit can become an explicit conflict, and a delayed snapshot cannot adopt another workout or account.

The proof is spread across the right owners. Core tests cover the new schedule shapes and compatibility. iOS tests cover first-workout presentation, calibration, swaps, Logger completion, model coding, and Watch-shared values. The wearable protocol has schema and fixture tests, while native Garmin tests exercise snapshot assembly and malformed input. The implementation is prepared for field testing on the selected Garmin profiles; simulator or build evidence alone does not establish physical-device acceptance, so I’m keeping that status precise.

What’s next

The next step is to run the complete first-workout journey against the built app and finish the physical Garmin acceptance matrix. I want the same thing from both paths: an athlete can choose a realistic plan, make the first set their own, and finish with a durable record whose owner and meaning are still clear.

This diary was generated and edited from the week’s commits, pull requests, tests, and development notes using AI, evidence-checked, and automatically published, and may describe work not yet publicly released.