A very big update, plenty of issues were fixed in this release, looking forward to better planning, fitting sportsmen of different level.
Removed
refresh_plan()removed — this function resetplan_start_dateto today, permanently
erasing prescription history for all sessions between the old and new anchor. There was no
recovery path. The timeline showed a gap; planned-but-unlogged sessions became unrecoverable.
The use case ("I want to skip ahead") doesn't require this: the plan already shows missed
sessions as-is, and the user can simply train the next upcoming session.set_plan_start_date
is retained for test fixtures and internal tooling.
Fixed
- 1RM estimation now uses a rep-range-aware formula throughout the planner — the
TM-derived fallback and history-based estimate in_estimate_effective_leff_1rm()both now
callbest_1rm_from_leff()(Brzycki+Lander blend ≤5 reps, +Epley ≤10, Lombardi+Epley
above). Previously, both paths applied a plain Epley formula capped at 12 reps. At TM > 12
(intermediate to advanced bodyweight athletes), the cap froze the estimated 1RM, producing
identical weight prescriptions regardless of actual strength — e.g., the same +6 kg for
pull-ups whether your TM was 12 or 25. - TEST session target now shows beat-last-result intent — the prescribed target reps for
a TEST session is nowround(TM / TM_FACTOR) + 1, which corresponds to one rep above the
athlete's last test result. Previously the target wasTM(90% of last test max), which
caused many athletes to stop at the displayed number rather than achieving a true max effort,
systematically suppressing TM over successive cycles. - 4-day schedule: S→H gap increased from 24h to 48h — day offsets changed from
[0, 1, 3, 5](Mon/Tue/Thu/Sat) to[0, 2, 4, 5](Mon/Wed/Fri/Sat). The 24-hour gap
between Strength and Hypertrophy sessions did not provide sufficient recovery for heavy
pulling or pushing. TheDAY_SPACING["S"] = 1constant inexercises.yamlwas not
enforced byschedule_builder.py(it is only read bytest_session_inserter.py); the fix
corrects the hardcoded day offsets directly. - Technique (T) session intensity corrected — T sessions were prescribed at 20–40% of TM
with RIR target 4–5, which for any athlete above TM=10 produced sets at RIR 10–15 (no
stimulus). Rep fractions updated to 0.45–0.65 (pull-up / dip) and 0.55–0.75 (BSS), RIR
target lowered to 2 across all four exercises. Sets reduced by 1 per level compared to H
sessions (T is quality work, not volume work). - Endurance (E) session volume formula removed — the
kE(TM) × TMformula targeted 84–150
total reps for TM 20–30, butsets_max=10andreps_max=8caps made the target structurally
unreachable: maximum achievable volume was ~45 reps regardless of TM. Every E session above
TM=12 was identical (10 sets, all capped). The broken formula has been replaced with
sets_by_level(same mechanism as S/H/T sessions): novice→advanced maps to
[6,7,8,10](pull-up),[5,6,7,8](dip),[3,4,5,5](BSS),[3,4,4,5](incline DB
press). Volume now grows as the athlete advances, with the descending rep ladder preserved.
endurance_volume_multiplier()removed fromconfig.py. - Autoregulation gates lowered —
MIN_SESSIONS_FOR_AUTOREGreduced from 10 to 3 (was
gating on 6–10 weeks for 3-day practitioners, leaving new users on a fully open-loop plan
too long). Readiness z-score thresholds narrowed:READINESS_Z_LOWfrom −1.0 to −0.5 and
READINESS_Z_HIGHfrom 1.0 to 0.5, so moderate fatigue and moderate freshness both now
influence prescription rather than only extremes.
[0.7.0] - 2026-04-03
Added
- Level-based adaptive set counts —
ExerciseDefinitiongainslevel_thresholds: list[int]
(e.g.[4, 13, 24]for pull-up, based on Strength Level database) and each S/H/T session
block gainssets_by_level: list[int](indexed 0=novice→3=advanced). The planner classifies
the user's level from their latest test max and prescribes the corresponding set count
directly. Replaces the fixed midpoint(sets_min + sets_max) // 2which always produced
the same value regardless of user progress. - Intra-session rep decay —
ExerciseDefinitiongainsset_fatigue_curve: list[float]
(e.g.[1.0, 0.85, 0.75, 0.68, 0.63]). Each set'starget_repsis multiplied by the
curve factor for its position, modelling empirical ~15–30% rep drop-off per set at moderate
rest (PMC11057609). Set 1 receives full reps; subsequent sets descend. E sessions (volume
ladder) and TEST sessions are unaffected. _classify_level(latest_test_max, level_thresholds) -> intinset_prescriptor.py—
returns 0-indexed level (0=lowest) using the first threshold the test max is ≤.
Falls back to middle level when either argument isNone.- All 4 exercise YAMLs updated with
level_thresholdsandset_fatigue_curve. S/H/T blocks
now usesets_by_leveland omitsets_min/sets_max(now optional, defaulting to 1/10).
Fixed
- Epley 1RM over-prescription for high-rep users —
_estimate_effective_leff_1rmand
the TM-derivedleff_1rm_tmformula both now cap reps at_MAX_EPLEY_REPS = 12before
applying1 + reps/30. Epley's formula is unreliable above ~12 reps. A dip user with
test_max=20 was previously prescribed +21.5 kg (impossible); now correctly ~+6 kg. - Grip rotation resumes correctly after deviant grip —
_init_grip_countsnow records
the last logged grip per session type and usescycle.index(last_grip) + 1as the rotation
position. Previously it counted total sessions, causing rotation to restart from the same
grip after any deviation (e.g. pronated → neutral → pronated-deviant → pronated again). - Autoregulation set floor no longer hardcoded at 3 —
apply_autoregulationaccepts
sets_min: int = 1and uses it as the floor when reducing sets for low readiness. The
oldmax(3, ...)over-prescribed for beginners whose level-based set count was 1–2.
Removed
assist_progressionfield removed from exercise YAML andExerciseDefinition—
resistance bands (BAND_SET) are now treated identically toMACHINE_ASSISTED: the
user declares available resistance values and the planner ceiling-snaps each session.
No automatic step-down from bands to unassisted. To graduate, the user removes
BAND_SETfromavailable_items.check_band_progression(data_dir, exercise_id)removed — no replacement;
automatic band step-down logic has been deleted.get_next_band_step(item_id, exercise_id)removed — no replacement.get_assist_progression(exercise_id)removed — no replacement.get_equipment_catalog()no longer returns"assist_progression"— return shape
is now{"default_item": str, "items": dict}.
Added
default_itemper exercise — each exercise YAML now declares adefault_item
(e.g.BAR_ONLYfor pull-up,PARALLEL_BARSfor dip,DUMBBELLSfor BSS/incline).
Exposed inget_exercise_info(),list_exercises(), andget_equipment_catalog().
Allows clients to pre-select a sensible starting equipment without hardcoding
exercise knowledge.requires_weight_declarationflag per catalog item — boolean field in each
equipment catalog entry (trueforBAND_SET,MACHINE_ASSISTED,DUMBBELLS).
Clients read this flag to know when they must prompt the user for specific kg values
before the item can be used by the planner.BAND_SETreplacesBAND_HEAVY/BAND_MEDIUM/BAND_LIGHT— resistance bands
now use the same model asMACHINE_ASSISTED: the user declares their actual band
resistance values in kg viaupdate_equipment(..., available_band_assistance_kg=[...]),
and the planner ceiling-snaps the computed ideal to the smallest available value ≥ ideal.
Eliminates the hardcoded midpoint estimates (17/35/57 kg) and the three-tier distinction.calculate_band_assistance()incore/planner/load_calculator.py— mirrors
calculate_machine_assistance()for band resistance values; both share a private
_calculate_variable_assistance()helper.available_band_assistance_kgfield onEquipmentStateand serialized toprofile.json.get_equipment_catalog()now returns a structured dict withdefault_item,
assist_progression, anditems(each item includesrequires_weight_declaration).
Breaking change from the previous flatdict[str, dict]return type.
Removed
BAND_HEAVY,BAND_MEDIUM,BAND_LIGHTequipment items frompull_up.yamland
dip.yaml(superseded byBAND_SET).machine_to_nearest_band()fromcore/equipment.py— no longer needed without
fixed band tiers.