Back to home
FormaForma

Training, holistically.

One clear read of your fitness.

Forma
Role
Design + code
Team
Solo
Timeline
8 weeks
Outcome
12 new PBs

Premise

I train anywhere between 8 to 15 hours a week. Running, riding, and swimming for triathlons, with strength workouts, yoga, and pilates holding it together. The apps I use are good at their slice of it. Strava sees a feed, Garmin sees a watch face, Runna only sees the running. None of them see the whole picture, and the whole picture is why I train the way I do.

So last April I started building the app that does. Forma reads my training from Strava, holds one picture of it across every sport, and tells me how ready I am today.

The fitness engine

Forma answers one question when I open it: how ready am I today. The number counts every activity I've done to date. Runs, rides, swims, lifts, yoga, all of it.

One honest number meant choosing the model myself. I went with an exponential heart-rate model from published sports science (Stagno's TRIMP-exp) because I could audit the math. I then calibrated it against my own training until the number matched how the weeks actually felt.

One rule below the interface keeps that number honest: the query behind the score can never filter by sport. Every run, ride, lift and yoga session counts toward the same total. And when the model is guessing, the card says so. Strength work is the usual case, because heart rate is a poor proxy for how hard you lifted.

Productive

Your fitness is climbing while fatigue stays in check. Energy is steady and the load is producing real fitness gains without overload. Recovery is keeping pace. If this pattern holds for another 2–3 weeks, fitness will compound further before a deload locks the gains in.

Cycle ‹ › through training statuses.

The journey at a glance

Every workout I've logged sits on one canvas. Five zoom levels, from the full history down to a single day. Eras name themselves from what I was training for. Gaps stay visible instead of getting smoothed over. Race days are flagged along the top.

Pan and zoom run on spring physics, so the view settles instead of snapping. Days worth remembering can be pinned, and the whole canvas works with keyboard shortcuts.

There wasn't a stock component I could use from a charting library, so I had to build it from scratch. About 1,600 lines of canvas code that I steered Claude through until the component matched the one in my head. It's the part of Forma I show people first.

EVERY ACTIVITY, EVERY DAY
DRAG TO PAN · ARROW KEYS TO NAVIGATE
SESSIONS TO DATE
since —
ACTIVE DAYS
of days
LONGEST STREAK
consecutive active days
REST DAYS
of days

Five view levels, pins, eras, minimap. Drag to pan, hover a day.

Forma's voice

Every session gets a short read, and every trend a name in plain words. A read might say a run sat in a narrow heart-rate band at steady effort. None of it comes from a language model though. The sentences are assembled by deterministic engines, and the voice is enforced by tests: the copy never prescribes, never mentions injury, never claims anything the data can't back. If a generated sentence breaks a voice rule, the build fails. That way, there's no drift in the product when I'm not looking.

THE VOICE, RUN AS TESTSVOICE RULES → CI
CLAIMS WHAT THE MODEL CAN'T SEECHEERLEADING
ONLY WHAT THE DATA SHOWSDESCRIBES, NEVER PRESCRIBES

A sentence the tests reject, then one they pass.

Building with AI

I designed Forma in the same place I built it, with Claude Code in the terminal as the engineering partner. Most of my effort went into context engineering, not prompts. Linear holds the product's memory (decisions, dependencies, deferred work as tickets). An Obsidian vault holds what each session taught me. A set of skills writes both at session close, so the next session starts with that context already loaded.

On a team, that memory lives in other people. Alone, it has to live somewhere the next session can read. It's also the efficient way to run a model: context windows are finite and tokens cost money, so each session loads only the tickets and notes it needs instead of re-deriving the project from scratch. A session-end audit prices the prompts in dollars and flags the ones worth tightening.

FORMA'S SECOND BRAINHOVER ON CHIPS
›_
The agent
REASON, ACT, OBSERVE, REPEAT
claude code claude design
The rules
OPERATING RULES, ONE PAGE
voices.mdfitness-fatigue-model.mddesign-tokens.md
Linear (product memory)
TICKETS · DECISIONS
FOR-168FOR-199FOR-200
Obsidian (session memory)
WHAT EACH SESSION TAUGHT
2026-06-05.md2026-06-01.md
The skills
WORKFLOWS AS MARKDOWN
/ session-learnings/ feature-kickoff/ linear-update/ code-review
THE NEXT SESSION STARTS ALREADY LOADED
EXAMPLE"store wall-clock dates, not per-user tz" · decided mid-session → logged by /session-learnings → loaded next session, already in context
FORMA'S MEMORY MAPPED
DRAG NODES · HOVER FOR CARDS
OPERATING RULESLIVING DOCSDOCS + PLANSSESSION LOGLINEAR TICKETS

Working solo also means having no one to review my work before it ships. So green tests get an adversarial review for a sanity check. My tests encode the same assumptions as my code, so they miss what I miss. Independent agents prompted to break the change come at it with failure hypotheses I didn't have, and running a panel of them in parallel costs minutes.

On a billing change, my tests passed, and the panel found a bug the tests had missed: returning subscribers would have been quietly treated as unsubscribed. Nothing would have crashed. It would have just been wrong for months.

npm test
✓ 618 passed, 0 failed (4.2s)
/code-review --panel 3 --scope src/lib/billing
loading context: the ticket · its plan file · 3 session-log hits
spawning 3 isolated reviewers, each seeded a failure hypothesis…
reviewer 1 · proration rounding drift    no repro · 14 paths traced
reviewer 3 · webhook double-charge       no repro · idempotency holds
reviewer 2 · returning-subscriber state  1 FINDING
FINDING · high · subscription-state.ts:87
  resubscribe after cancel → status stays "unsubscribed"
  repro: cancel → period lapse → checkout.session.completed
  why tests missed it: fixture set never re-subscribes
panel: 3 reviewers · 11 hypotheses · 1 finding · 6m12s · $0.84

The same partnership runs the economics. When Forma needed real map tiles, Claude and I priced the options across a few rounds, and the answer was self-hosting the entire planet with Protomaps (126 GB of vector tiles on Cloudflare R2). Storage costs about $2/month, egress is free, and the cost stays flat no matter how many people look at a map.

It wasn't the fastest to build or the fastest to load. It was the cheapest to run, which is what matters while I'm still validating the hypotheses, the designs, and the product decisions.

126 GB
FILE SIZE
$2/mo
R2 STORAGE
$0
EGRESS
1 file
NO TILE SERVER

Building this way made me more precise than I've ever had to be. Most of that precision goes into knowing what to build, saying it exactly, and writing down the rules the product must never break.

What changed

I used to push through tired weeks on instinct, because I had no way to see the load I was carrying. Now the fitness curve tells me when I'm carrying too much, and I take the day off without second-guessing it.

The gear page did the same for my shoes. I found out within the first week that I was overusing one pair and barely touching another, so the rotation is deliberate now. And the weeks I used to write off as rest turned out to carry real load. A heavy yoga week lands in the same number as a heavy run week, just shaped differently. I train differently because I can finally see that.

SHOE ROTATION · RUN DAYS
WEEK 1
BOSTON 13
4
EVO SL
0
WEEK 2
BOSTON 13
3
EVO SL
1
WEEK 3
BOSTON 13
2
EVO SL
2
WEEK 4
BOSTON 13
2
EVO SL
2

Four weeks of run days: from a 4–0 split I couldn't see to a rotation that fits my training.

How I build changed with it too. I don't start designs in Figma anymore. I start in the terminal, and the first version of an idea is a working one, so I decide direction by using real prototypes instead of reviewing static mocks. Decisions I used to hand off in documents now ship as constraints the code enforces. And I measure the performance questions before building anything.

The training caught up too. I've set personal bests this year I didn't think were possible for me, and it wasn't just from training harder. It came from logging everything, learning how each sport and each rest day moves my form, fitness, and fatigue, and planning in seasons instead of weeks. I'm looking forward to what the next block brings.

Crossing the finish line of a triathlon in the rain, arms raised

Completing my first triathlon. First step toward an Ironman.