Speaks Up

Two versions ago, the Stats tab learned to notice imbalance. Last month, it learned to suggest a fix. v1.36 closes the loop: when the same imbalance sticks around for weeks, the app volunteers the fix on Home — before you open Stats, before you plan the week, before you have to notice.

The arc

v1.34 shipped Balance Advice: a Stats-tab card that named the lagging family and pointed at the specific add-on that would fix it. Users could tap through to StartProgram and do the session right then.

v1.35 added Add to this week: one tap injected the add-on into the current week's plan, on the nearest available weekday. Manual, single-week, deliberate.

Both required the user to open Stats and see the card. If the imbalance was chronic — three weeks of Newcomer core against Advanced pull — the burden of noticing sat with the user every single time.

v1.36 is the piece that removes the noticing.

What Level 3 does

Every /home request the app makes, the backend runs a persistence tracker. It looks at your current weakest family and asks: how long has this been the weakest?

Less than three weeks — silent. Week-to-week variation is normal; nobody wants a nag.

Three weeks or more — the tracker checks a few gates. Did the user decline a proposal for this family in the last 90 days? Are they already three weeks into an accepted proposal? Did they manually inject a balance day recently? If any of those, still silent — the app respects prior decisions.

Three weeks, no gates triggered — a card appears at the top of Home, above today's workout. It names the family, says how long it's been lagging, names the specific add-on program, and shows the cadence: "For 3 weeks, run Bulletproof Shoulders (Beginner) 2 sessions a week."

Two buttons: Add to my plan and Not now.

Add to my plan schedules the sessions into the current week automatically, tags them with the proposal id, and stamps the plan history. When the week rolls over, the sessions stay. When the three weeks end, the sessions disappear — cleanup runs silently on the next plan read.

Not now stamps a 90-day quiet period for that family. The app won't propose anything for that family again until the quarter passes.

Why the specific choices

Three weeks. Below that, imbalance is noise. Above that, it's a pattern. The 3-week threshold is the same coach-noticing zone the v1.34 "3 tier gap" number came from — where a real trainer would start bringing it up.

Quiet periods respect prior decisions. If you said no, the app doesn't ask again for 90 days. If you accepted and are currently running the program, we don't propose a second thing on top. If you revoked mid-way, that's a stronger signal than a decline — same 90-day quiet. This matters because a proactive nudge system that ignores decisions becomes noise fast.

Additive, never replacing. The proposal adds sessions to your existing plan; it never removes or rearranges what's already there. If your plan says Monday Pull, Wednesday Push, Friday Rest, and we schedule Bulletproof Shoulders Tuesday + Thursday, all your existing days stay put.

Revocable. Accepting doesn't lock you in. The plan editor will let you back out mid-cycle (a small follow-up patch is authoring that UI). The persistence design already exists on the backend — the sessions are tagged and one call removes them all.

Home, not push. The card lives on Home because that's where you land when you're planning your week. A push notification for a multi-week commitment would land in your notification tray and get swiped away between other pings. On Home, the ask is contextual and reversible.

What this replaces

Not much, actually. Balance Advice on Stats is still there for users who want to browse their state. Add to this week is still there for one-off tactical injections. Level 3 slots in above both — the app volunteering an ask that used to require the user to notice.

If you never see the card, you're balanced or your imbalance is fresh. If you see it, the pattern is real and the app has the fix.

Under the hood

  • Persistence tracker + 4 quiet-period gates (declined 90d / accepted 28d / revoked 90d / manually-injected 14d). 15 unit tests covering all threshold boundaries and the interaction with the existing pendingProposal state.
  • Three new endpoints: /plan/proposal/accept creates tagged PlanDay rows for the current week, /decline stamps history, /revoke deletes tagged rows. 11 mocked-prisma route tests covering happy paths + guards.
  • Auto-expiry sweep in the weekly rotation logic — no cron dependency; cleanup runs the same tick the plan advances the week.
  • Four analytics events: plan_proposal_shown / _accepted / _declined / _revoked. Sliced by family + weeks-lagging + program-id so we can see whether the coach voice actually converts and which imbalance shapes users take seriously.

What's next

If Level 3 converts — that is, if users accept and complete cycles at meaningful rates — the natural next step is per-skill balance advice: the same persistence logic applied to Handstand, Planche, and the other skill families. That's Level 4 territory, and it depends on the same signal ADR-003 fed into the Stats-tab drilldown.

If it doesn't convert — if the ask feels premature or the cadence is wrong — we tune the thresholds server-side without a build.

For now: keep training, and when the app speaks up, hear it out.

Available in v1.36.0 on TestFlight now. Rolling out to the App Store this week.