- opencode agent (fitness-trainer) for personal training conversations - fitness-workout skill with programming methodology guidelines - workout.py script (1RM, volume, cycle helpers) - logs/ directory for workout and check-in markdown files - inputs/ with user profile (equipment, goals, medical, Juggernaut history) - google-sheets-automation skill for optional Sheets integration - AGENTS.md with setup documentation
120 lines
3.3 KiB
Python
120 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Helper utilities for the fitness trainer agent.
|
|
|
|
Usage:
|
|
workout.py e1rm <weight> <reps> # Estimate 1RM (Epley)
|
|
workout.py volume <sets> <reps> <weight> # Total tonnage
|
|
workout.py training-max <e1rm> [pct] # Training max (default 90%)
|
|
workout.py cycle <tm> <rep-scheme> # Suggest weights for a wave
|
|
|
|
Examples:
|
|
workout.py e1rm 185 5
|
|
workout.py volume 3 5 185
|
|
workout.py training-max 210
|
|
workout.py cycle 200 5x5
|
|
"""
|
|
|
|
import sys
|
|
import math
|
|
|
|
|
|
def e1rm(weight: float, reps: int) -> float:
|
|
"""Estimate 1RM using the Epley formula."""
|
|
if reps == 0:
|
|
return weight
|
|
if reps == 1:
|
|
return weight
|
|
return weight * (1 + reps / 30)
|
|
|
|
|
|
def volume(sets: int, reps: int, weight: float) -> float:
|
|
"""Calculate total tonnage."""
|
|
return sets * reps * weight
|
|
|
|
|
|
def training_max(e1rm_val: float, pct: float = 0.90) -> float:
|
|
"""Calculate training max as a percentage of e1RM."""
|
|
return e1rm_val * pct
|
|
|
|
|
|
def round_to_nearest(weight: float, increment: float = 5.0) -> float:
|
|
"""Round weight to the nearest plate increment."""
|
|
return round(weight / increment) * increment
|
|
|
|
|
|
def suggest_cycle_weights(tm: float, sets: int, reps: int, steps: int = 4):
|
|
"""Suggest wave weights from ~80% to ~95% of training max.
|
|
|
|
Args:
|
|
tm: Training max
|
|
sets: Sets per exercise
|
|
reps: Reps per set (target)
|
|
steps: Number of weeks in the wave
|
|
"""
|
|
print(f"Training max: {tm}lb")
|
|
print(f"Scheme: {sets}x{reps}")
|
|
print()
|
|
for week in range(steps):
|
|
pct = 0.80 + week * 0.05
|
|
week_weight = round_to_nearest(tm * pct)
|
|
print(f" Week {week + 1}: {int(pct * 100)}% = {int(week_weight)}lb")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
if cmd == "e1rm":
|
|
if len(sys.argv) < 4:
|
|
print("Usage: workout.py e1rm <weight> <reps>")
|
|
sys.exit(1)
|
|
w = float(sys.argv[2])
|
|
r = int(sys.argv[3])
|
|
est = round_to_nearest(e1rm(w, r))
|
|
print(f"e1RM: {int(est)}lb")
|
|
|
|
elif cmd == "volume":
|
|
if len(sys.argv) < 5:
|
|
print("Usage: workout.py volume <sets> <reps> <weight>")
|
|
sys.exit(1)
|
|
s = int(sys.argv[2])
|
|
r = int(sys.argv[3])
|
|
w = float(sys.argv[4])
|
|
print(f"Volume: {int(volume(s, r, w))}lb")
|
|
|
|
elif cmd == "training-max":
|
|
if len(sys.argv) < 3:
|
|
print("Usage: workout.py training-max <e1rm> [pct]")
|
|
sys.exit(1)
|
|
e = float(sys.argv[2])
|
|
p = float(sys.argv[3]) if len(sys.argv) > 3 else 0.90
|
|
tm = round_to_nearest(training_max(e, p))
|
|
print(f"Training max: {int(tm)}lb")
|
|
|
|
elif cmd == "cycle":
|
|
if len(sys.argv) < 4:
|
|
print("Usage: workout.py cycle <tm> <rep-scheme>")
|
|
sys.exit(1)
|
|
tm = float(sys.argv[2])
|
|
scheme = sys.argv[3]
|
|
try:
|
|
parts = scheme.lower().split("x")
|
|
sets = int(parts[0])
|
|
reps = int(parts[1])
|
|
except (IndexError, ValueError):
|
|
print(f"Invalid scheme '{scheme}'. Use format like 3x5 or 5x5.")
|
|
sys.exit(1)
|
|
suggest_cycle_weights(tm, sets, reps)
|
|
|
|
else:
|
|
print(f"Unknown command: {cmd}")
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|