liftOS
Updates & iterations
The Lab goes native on iPhone
A SwiftUI and SceneKit app carries the whole anatomy atlas and eight movements, and works with no network.
The browser Lab was always meant to be a working surface, not the destination. In September it became a native iPhone app: SwiftUI controls around a SceneKit viewer, with the standing body and all its layers (skin, fascia, muscle, bone, cartilage, teeth and air spaces) plus the eight movement exhibits. Everything ships inside the app, so it works on first launch with networking off. Tap a structure to see its name; controls fade after three seconds and come back with a single tap.
The squat keeps its proportion explorer for femur, shank and torso, and the pose is now computed on the phone in Swift instead of in JavaScript. Against the web solver it matches across 37,740 matrices, with a worst-case difference of 2.06e-7.

Verification screenshots from the iOS Simulator, committed with the native build: the standing body, and the squat with its proportion sliders.
Content updates had to be boring and safe. Each release is a set of hashed files: 51 assets, about 207 MB. The app only switches to a new version after every file is verified, and a failed or interrupted download keeps the last complete one. Tests cover corrupt hashes, invalid models, path traversal and recovery from a half-finished update. What I have not claimed: the evidence is from the iOS Simulator, so frame rate, heat and gesture feel on a physical iPhone are still unverified, and the first device install was blocked on Xcode provisioning.
A playbook, and a register of everything that slipped past
After the Movement Combine, I wrote down the method that produced the one accepted lift, and started cataloguing defects by class rather than by incident.
After a retrospective of the whole lab arc, I turned the loop that produced the accepted deadlift into a binding playbook. Write the reference spec before any code. Decide what the equipment is supposed to touch before writing a single rule about it. Measure the exported, deformed model rather than what the solver intended. Judge motion at full speed, never stills. Ship with declared defects instead of hidden ones.
Next to it sits the defect register. Every time I have to point at something on screen, the process failed before I looked, so each report now has to end with three things: the defect measured as a number, a gate that is shown to fail on the old build and pass on the new one, and an entry describing the class of defect, not just the instance. The first entry is the barbell passing through both thighs on 21 of 97 frames, up to 13.9 mm deep. Its gate fails the old model at 13.7 mm and passes the new one at 0.00 mm.

The defect register as it stands in the repo today, generated from docs/movement-combine/DEFECT-REGISTER.md. It started with D-1 and has grown with every report.
The retro found the biggest losses were nameable dead ends: eight grip rounds with no written reference, checks that measured where the solver put the hand while the rendered hand stayed open, and one tolerance band that was not just blind to the bar going through the leg but actually required it. A register of classes lets a future session recognise the next defect before I have to screenshot it. It also says plainly which classes nothing catches yet, because an honest gap is more useful than a clean-looking list.
Recovery heat moves onto the 3D body
The same fatigue math that shades the 2D recovery map now paints the full anatomical model in the Lab.
Since June, the home screen has answered "what is still tired?" with a front and back figure. Every logged set contributes heat by muscle role and effort, the heat halves every 48 hours, and it is measured against your own trailing 28 days, so hot means hot for you rather than hot compared to someone else.
In August that signal reached the 3D anatomical body in the Lab. A small, tested module converts the per-muscle recovery values into the model's heat texture, rebuilt once per change. It needed no new mapping table: the atlas's own muscle labels match the engine's 27 muscle names exactly, and a test now fails if they ever drift apart.

Where it started: the 2D recovery map on Home, front and back, light and dark. Demo data.

The Lab's 3D body with heat painted per muscle. Signed out, so it shows the labelled sample heat. Rendered in headless Chromium with software WebGL.
I care that the body never claims more than it knows. Signed out, in demo mode or with no training data, the Lab shows sample heat and says so. The About panel only says "your recovery" when it really is yours. The muscle labels were built twice, independently, and compared before I trusted them, because naming the wrong muscle on an anatomy model is a medical claim.
The movement becomes the screen
Form-demo clips grew from a framed card into a full-bleed hero, with the coach's voice tied to the clip.
Exercise screens used to open on names, badges and numbers. Once I had form-demonstration clips, the order flipped: the movement fills roughly two thirds of the screen, edge to edge, and the written cues, your progress chart and your history scroll in underneath. Each clip keeps its own aspect ratio, because cropping a portrait squat to a landscape frame hides the depth, which is the thing you came to see.
The coach is no longer a separate module. The voice track plays from the top each time the clip scrolls into view, pauses when it leaves, and replays when you come back, so returning to the movement re-delivers the cue. A pause you make yourself sticks; only pauses the system made resume on their own. The letterbox around each clip is painted in that clip's sampled backdrop colour so the frame reads as one continuous studio.
Barbell Back Squat in the Library, with demo data. The clip came from my AI video pipeline, which I later retired in favour of the 3D movement engine. Recorded frame by frame in a headless browser, so the coach audio is not in the capture.
That morning I had shipped a smaller version: a framed clip, and a folded "Form demo" strip on the workout card. Seeing it in the browser made it obvious the video was the content, so the strip went and the clip took the screen. A review of the build found eight defects before commit, including a coach pill that could get stuck, no affordance when the browser blocks autoplay, and explicit pauses being overridden by scrolling. All eight were fixed in the shipped version.
The logging loop, and an engine that writes your next set
The core of the app landed in one overnight run: one-tap logging, a rest timer, and a deterministic engine that explains every prescription in a sentence.
The first real milestone was the path you use every session. Start a workout, see each exercise pre-filled, log a set with one tap, say how hard it was, and rest on a timer that starts by itself. Every write lands on the device first, so a dropped phone or a closed tab never loses a workout, and a set only becomes a record once you confirm it.
The progression engine sits underneath. Given an exercise, your history and your training profile, it prescribes every set and tells you why in plain English: "Last time: 6, 6, 6, 6 × 330.5 lb. Same weight, one more rep per set on the way to 12s." No randomness, no network, no AI. The same inputs always give the same answer, because the whole point is that you trust it enough not to override it.

Right after logging a warm-up: the effort question, the remaining prescribed sets and the auto-started rest timer. Captured from the app running on demo data, in today's visual design.
I had an adversarial multi-agent review attack the engine before I accepted it. It found two real violations of my own spec: a "repeat this weight" could quietly round up after a max-effort session, and a small layoff penalty could be rounded away and then allow an increase. Both are fixed, and the rule is now explicit: repeated loads are never rounded, reductions round down, increases step up. Mutation testing also showed that aggression levels 2 and 4 were untested, so a dedicated suite pins them. 78 tests were green when it shipped.