agentsclimarketplace

Plan builder

Skill kaustin923/agent-fitness-coach/.claude/skills/plan-builder

Give this repo to Claude and it becomes your training coach: real periodized plans, Strava + Apple Health data, progress tracking and grading — files are the database, skills are the features, the agent is the app.

Install
npx -y skills add kaustin923/agent-fitness-coach --skill plan-builder

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Interview the athlete, verify their fitness against real training data, and build a complete multi-week training plan — writes athlete/plan.json (canonical) and renders athlete/PLAN.md. Use when the athlete asks for a training plan, a race build ("train me for a half marathon"), a lifting program, wants to restart structured training, or pastes a plan from elsewhere that they want to adopt.

SKILL.md

20.9 KB, as published. Nobody here has run it

Plan builder

You are about to author a real training plan someone will follow for weeks. Two things make it trustworthy: it is grounded in what the athlete has actually been doing (not what they remember doing), and it passes a set of programmatic integrity checks before they ever see it. Never skip either.

When to run

  • The athlete asks for any kind of training plan: race prep, general fitness (walking counts), endurance base, strength block, weight loss.
  • Onboarding just finished and the athlete's goal calls for a plan.
  • The athlete pastes a plan from a coach, a book, or another app — never refuse to import it. Extract every slot you can from the paste, confirm your reading in one message, and build in the same turn. Only ask about a core slot that is genuinely missing and has no sensible default.
  • A goal or race change makes the current plan fundamentally wrong (a tweak is plan-adjust; a new race distance or new goal type is a rebuild).

Inputs

  • athlete/profile.md — identity, goal, schedule constraints, declared units, any stored easy pace. If there is no profile at all, run the onboarding skill first. If the profile exists but relevant fields sit on the missing: list, don't send the athlete back to onboarding — offer to fill each gap in one question here, or proceed on a recorded assumption (an entry under the profile's assumptions: list) with a one-line caveat they can correct anytime. The athlete picks.
  • athlete/plan.json — if an active plan exists, confirm before replacing it ("You have an active plan through {end_date} — replace it?"). On confirmation, set the old plan's meta.status to "archived" and offer to save a dated copy before overwriting.
  • athlete/log.json and athlete/health/workouts.csv — recent training evidence.
  • Strava MCP tools when connected: list_activities, get_athlete_profile, get_athlete_zones (exact prefixes vary by harness, e.g. mcp__claude_ai_Strava__list_activities).
  • Reference files: reference/training-rules.md, reference/pace-zones.md, reference/safety.md, templates/plan.schema.json, templates/PLAN.example.md.

Procedure

1. Dates first

Compute today with date +%F and build the weekday↔date table for this week and next (the COACH.md ritual). Every date you touch from here on is paired with its weekday, and every weekday you mention comes from a computed table — never from your own calendar arithmetic. For race plans, compute the race date's weekday in python and say it out loud ("October 11 is a Sunday").

2. Gather the slots — a conversation, not an interview

Weave questions naturally, 1–2 per turn, reacting to answers. Never dump a numbered questionnaire. The checklist is for you, not the athlete.

The core slots, each with a defer-to-coach default:

SlotValuesIf the athlete defers
Goal typerace, general_fitness, build_endurance, build_strength, lose_weightinfer from profile goal, else general_fitness
Modalitiesrunning, lifting, walking, or a mixrunning + lifting, unless one is clearly excluded; walking-first for a general-health athlete who doesn't run
Days per week1–74
Experiencebeginner, intermediate, advancedintermediate
Durationgeneral plans 4–24 weeks; race plans ~10–18 weeks (per reference/training-rules.md, "Periodization")8 weeks; for races, weeks until race date, clamped to ~10–18

Race plans additionally need: race type (5K, 10K, half marathon, marathon, ultra, other), race date (in the future — check against today; the one slot with no default, since a race plan can't exist without it), target time or an explicit "just finish" (the default when they shrug), preferred long-run day, and rest day(s) (the exemplar's Saturday-long/Sunday-rest is the default if they wave these off). Rest-day capture is must-pass for every goal: any stated rest or training-day preference ("weekends off") becomes structured data, not a nodded acknowledgment.

High-value optional slots — ask when relevant, never block on them: equipment access (full gym, home gym, minimal), running background (new to running, some experience, solid base, experienced), current weekly mileage, recent race times, injuries (running and lifting separately), max lift session length (15–240 min), a known easy pace ("do you have a typical easy pace, or should I pick one from your race target?"), and firm formatting preferences (the whole-mile rule below started life as one athlete's firm preference).

Phrase-to-duration map: "until race day" → derive from the date; "short" or "quick start" → 4 weeks; "standard" → 8; "long" or "full block" → 12. "You decide" / "use defaults" → stop asking and build with the defaults above.

Always end gathering with one open question: "Anything else I should know — past injuries, schedule constraints, equipment, things you hate?" Whatever comes back is honored with the same force as a structured answer: an injury mention bans the named movements and everything that loads the same joints.

3. Verify from data — trust observed over stated

Before designing anything, pull the last 28 days of training. Best source first: Strava MCP list_activities; else athlete/health/workouts.csv or athlete/log.json; else the athlete's stated numbers, flagged as unverified. Compute in python, per reference/training-rules.md ("Grounding a plan in real data"):

  • Last 7 days: run count, total miles, average pace, longest run.
  • Days 8–28: totals ÷ 3 = trailing weekly baseline (miles, pace, lift sessions per week).
  • Days since last run and last lift.

Apply the grounding rules: observed pace and mileage outweigh self-report roughly 2:1 (people underestimate their easy pace and overestimate their mileage). If last-7-day volume is below 0.7× the trailing baseline (the deload trigger defined in reference/training-rules.md, "Grounding a plan in real data"), build a deload into week 1. If the data contradicts what they told you ("I haven't been running" but Strava shows four runs), ask once to confirm — don't silently pick a side. Never prescribe 5 miles a week to someone who just ran 30.

4. Sanity-check the goal

For race goals with a recent race on record, run the Riegel prediction (see reference/pace-zones.md, "Riegel race-time prediction"): T2 = T1 × (D2/D1)^1.06. Compare to the target and deliver a verdict:

  • Conservative (goal slower than predicted): keep the goal, and record a mid-build decision point in the profile ("if MP long-run segments feel easy for 2+ consecutive weeks, revisit the target").
  • At fitness: proceed.
  • Aggressive (goal meaningfully faster than predicted): say by how much, once, with the specific risk, and offer a longer build, an intermediate tune-up race, or a two-stage target. Build to what they choose — it's their race. The verdict goes in the profile and the choice in meta.methodology; after that, no re-warning in later sessions, and no quietly training them toward the softer number.

5. Design the plan

Everything numeric comes from the reference files — do not improvise constants. The reference values are defaults, not laws for the athlete: when they've explicitly chosen something outside a guardrail (a faster ramp, more days, a longer long run), give one concise, specific risk note, then design to their choice and record it in meta.methodology — never quietly re-shape the plan back toward the default.

  • Paces: derive the zone table per reference/pace-zones.md — from the goal time (offset ladder) for race plans, from the observed easy pace or a recent race (fitness ladders) otherwise, with experience-level defaults as the last resort. Run every zone through the sanity checks ("Server-style sanity clamps"): within ±20 s/mi of its ladder-derived value and inside the absolute validity windows. Store and display paces in the athlete's declared units.
  • Periodization: phase splits, cutback cadence (every 3rd–4th week), and taper length by race distance per reference/training-rules.md ("Periodization", "Taper and race week").
  • Volume ladder: weekly growth ≤ ~15% on load weeks, long-run jumps ≤ 2 miles, cutbacks −20–25%, post-cutback weeks resume just above the prior peak ("Weekly volume progression").
  • Weekly microcycle: the plan bends to the athlete's firm schedule, never the reverse. Long run on their declared day, rest on their declared days, at most two quality sessions per week, hard days never adjacent.
  • Workouts: construct each session from the templates in "Workout construction". Use the exact prescription-text conventions — intervals R x D mi @ {range} w/ J mi jog (W mi) where W = R×D + (R−1)×J (recovery jogs sit between reps only), tempo N mi @ {range}, long runs E mi @ {easy range}, last M mi @ MP {pace} (E is the day's total, inclusive of the M-mile MP finish), shakeouts N mi easy @ {range} + K x 100m strides.
  • Strength: per "Strength programming" — lower body ≥ 48 h from any hard run, upper body on no-run days, RPE 7 / ~2 reps in reserve during run builds, exercise selection rotating on a 4-session cycle. Strength on a run day goes in the day's strength string (sidecar form); standalone lift days use type: "Lift" with duration_min and lift_details.
  • Walking / general health: walking-first plans are prescribed by minutes, not miles, per reference/training-rules.md ("Walking and general-health progression") — weekly-minute growth capped like the mileage ladder, one long walk per week, brisk-walk intensity cue, optional light-strength pairing. Walk sessions are type: "Walk" days with duration_min set; they carry no run miles, so the whole-mile rule doesn't apply to them — and unlike bonus Cross-Train movement, they count toward completion and the training streak.
  • The whole-mile rule (running plans): every run session totals a whole number of miles — the invariant applies to the session total, not each segment. On quality days, size the warm-up (1–2 mi in 0.5-mi steps) and cool-down so the sum lands whole — a work block ending in .5 gets a 1.5-mi warm-up to absorb it — with interval recovery jogs of 0.25 or 0.5 mi (or by time), per the exemplar templates in "Workout construction". Race day (26.2) is the sole intentional exception. This is a preference rule, on by default — an athlete who'd rather not round waives it, with the waiver recorded in meta.user_preferences. See "The whole-mile rule and other invariants".
  • Calendar: weeks run Monday–Sunday. Anchor race plans so race day falls in the final week (start Monday = the Monday weeks back from race week). Compute every date in python; if the athlete named rest weekdays, generate the literal list of matching dates across the whole span — including partial first and last weeks — and make each one a Rest day.

6. Write plan.json

Emit athlete/plan.json conforming exactly to templates/plan.schema.json:

{
  "meta": {
    "athlete": "First name",
    "goal": "Chicago Marathon",
    "goal_type": "race | general_fitness | build_strength | build_endurance | lose_weight",
    "race_date": "YYYY-MM-DD",
    "target": "3:45:00",
    "start_date": "YYYY-MM-DD",
    "end_date": "YYYY-MM-DD",
    "weeks": 15,
    "days_per_week": 5,
    "includes_running": true,
    "includes_lifting": true,
    "methodology": "free text: anchors + key decisions",
    "status": "active | pending | completed | archived",
    "generated": "YYYY-MM-DD",
    "user_preferences": "free text: firm constraints"
  },
  "paces": { "recovery": "9:45-10:15", "easy": "9:15-9:45", "marathon": "8:30-8:40", "tempo": "7:55-8:10", "interval": "7:10-7:25", "strides": "~6:30" },
  "weeks": [
    {
      "index": 1,
      "phase": "Base | Build | Peak | Taper | Race",
      "weekly_miles": 26,
      "cutback": false,
      "focus": "one-line focus",
      "days": [
        {
          "date": "YYYY-MM-DD",
          "weekday": "Mon",
          "type": "Easy | Recovery | Tempo | Interval | Long | Marathon-Pace | Walk | Cross-Train | Rest | Race | Lift | Shakeout",
          "title": "Tempo · 5 mi @ threshold",
          "total_miles": 7,
          "warmup": "1 mi @ Easy",
          "workout": "5 mi @ 7:55-8:10",
          "wo_miles": 5,
          "cooldown": "1 mi @ Easy",
          "pace_zone": "tempo",
          "duration_min": null,
          "strength": "Lower body (light, 2 RIR)",
          "lift_details": null,
          "status": "scheduled | completed | skipped",
          "actual": null
        }
      ]
    }
  ]
}

Field semantics that trip people up: wo_miles is the whole work block including recovery jogs; warmup/cooldown are display strings (recompute numbers as total_miles − wo_miles or by parsing); weekly_miles sums run miles only — Walk, Cross-Train, and Rest contribute 0, but a Race day's miles count; non-running days may omit run fields; every day starts status: "scheduled" with actual: null. Run distance fields (total_miles, wo_miles, weekly_miles) are stored in miles for every athlete — a storage convention only; the athlete's declared units govern all rendering, and the paces strings are stored in the declared units. Walking-only plans may store paces as an empty object {} — intensity cues live in each day's workout text. Set meta.status to "active" and record your key design decisions in meta.methodology (goal verdict, anchor evidence, cutback/taper placement).

7. Run the invariant checklist — arithmetic is absolute, preferences are the athlete's

The checks come in two kinds. Arithmetic and structural integrity (2, 3, 4, 6, 7, 9 below — sums that must add up, dates that must agree, the athlete's own rest days honored, paces derived correctly) always binds: the original generator exits with "Plan has fractional miles — fix before shipping." rather than write a broken plan, and you hold that same standard — fix arithmetic violations before presenting, every time. Preference and methodology rules (1, 5, 8 — whole miles, progression limits, lift spacing) apply as the athlete has them set: on by default, but a deviation they explicitly chose (one risk note, then their way) is recorded in meta.methodology and the check conditioned on it — recorded, not refused. Verify programmatically, in python — never by eyeballing:

  1. Whole miles: every run day's total_miles is an integer (Walk, Cross-Train, Rest, Lift, and Race are exempt; 26.2 is the only sanctioned fraction).
  2. Segment sum: on any day with a warm-up or cool-down, warm-up + wo_miles + cool-down = total_miles. Continuous runs (no warm-up, no cool-down) set wo_miles = total_miles instead.
  3. Interval arithmetic: work block = reps × rep distance + (reps − 1) × jog.
  4. Week sum: weekly_miles equals the sum of its days' miles.
  5. Progression: load-week volume growth ≤ ~15% over the prior peak; long-run jumps ≤ 2 mi over the prior peak (cutback and taper weeks exempt).
  6. Calendar: every weekday matches its date (computed, not assumed); days run Mon–Sun without gaps; race day equals meta.race_date; all dates fall inside [start_date, end_date].
  7. Rest days honored: every date on the must-rest list is type: "Rest", including partial weeks.
  8. Recovery spacing: no heavy lower-body lift within 48 h of a hard run (Tempo, Interval, Long, Race).
  9. Paces in bounds: every prescribed pace sits within ±20 s/mi of its ladder-derived zone value and inside the absolute validity windows — both per reference/pace-zones.md ("Server-style sanity clamps").

A starting point (extend for checks 3, 7, 8, 9):

import json, re, datetime as dt

plan = json.load(open("athlete/plan.json"))
errs = []
RUN = {"Easy", "Recovery", "Tempo", "Interval", "Long", "Marathon-Pace", "Shakeout"}

def leading_mi(s):
    m = re.match(r"\s*([\d.]+)\s*mi", s or "")
    return float(m.group(1)) if m else 0.0

peak_vol = peak_long = 0
for wk in plan["weeks"]:
    total = 0.0
    for d in wk["days"]:
        mi = d.get("total_miles") or 0
        total += mi
        tag = f"W{wk['index']} {d['date']}"
        wd = dt.date.fromisoformat(d["date"]).strftime("%a")
        if wd != d["weekday"]:
            errs.append(f"{tag}: weekday '{d['weekday']}' but calendar says {wd}")
        if d["type"] in RUN:
            if float(mi) != int(mi):  # preference rule — drop if the athlete waived whole miles
                errs.append(f"{tag}: fractional session total {mi}")
            wu, cd = leading_mi(d.get("warmup")), leading_mi(d.get("cooldown"))
            if d.get("warmup") or d.get("cooldown"):
                if abs(wu + (d.get("wo_miles") or 0) + cd - mi) > 0.01:
                    errs.append(f"{tag}: wu+work+cd != total {mi}")
            elif d.get("wo_miles") not in (None, mi):
                errs.append(f"{tag}: continuous run wo_miles {d['wo_miles']} != total {mi}")
    if abs(total - wk["weekly_miles"]) > 0.05:
        errs.append(f"W{wk['index']}: weekly_miles {wk['weekly_miles']} != day sum {round(total, 1)}")
    longs = [d["total_miles"] for d in wk["days"] if d["type"] == "Long"]
    # progression checks are methodology defaults — exempt weeks the athlete knowingly chose to push (see meta.methodology)
    if wk["phase"] in ("Base", "Build", "Peak") and not wk.get("cutback"):
        if peak_vol and total > peak_vol * 1.15 + 0.01:
            errs.append(f"W{wk['index']}: volume {round(total, 1)} grows >15% over peak {peak_vol}")
        if longs and peak_long and max(longs) > peak_long + 2:
            errs.append(f"W{wk['index']}: long run {max(longs)} jumps >2 mi over peak {peak_long}")
        peak_vol = max(peak_vol, round(total, 1))
        peak_long = max(peak_long, max(longs) if longs else 0)

race = plan["meta"].get("race_date")
if race and race not in [d["date"] for w in plan["weeks"] for d in w["days"]]:
    errs.append(f"race_date {race} is not a day in the plan")

print("\n".join(errs) or "Invariant check: OK")
if errs:
    raise SystemExit("Plan has violations - fix before presenting.")

On any arithmetic violation: fix the plan, re-run, repeat until clean — presenting a plan that fails arithmetic is the plan-builder equivalent of hallucinating. A flagged preference-rule deviation the athlete explicitly chose is different: it isn't a violation, so condition that check on their recorded choice rather than "fixing" the plan they asked for.

8. Render PLAN.md

athlete/PLAN.md is a projection of plan.json — regenerate it from the JSON, never hand-edit it. Conventions (match templates/PLAN.example.md): title # {athlete} — {goal} Training Plan; a goal line; the paces table up top; per week a ## Week {index} — {phase} · {weekly_miles} mi heading, an italic _{focus} · begins {Monday's date}_ line, and a | Day | Date | Type | Warm-up | Workout | Cool-down | Mi | table with MM-DD dates, an em-dash for 0-mile days, blank cells where fields are empty, any | inside workout text replaced with /, and strength cues as an italic sub-row.

9. Update the profile

Write to athlete/profile.md: the derived pace zones, the goal verdict with its evidence, and any watch-list items (mid-build upgrade decision point, heat caveats, injury history to monitor).

Rules

  • plan.json is the single source of truth; PLAN.md and the dashboard are projections of it.
  • All arithmetic — paces, dates, mileage sums, Riegel — runs in python. Model mental math is how plans ship broken.
  • Never state a weekday without its date, and never derive a weekday in your head.
  • The athlete's firm constraints are structure, not suggestions. Bend the template, not the schedule — and their wishes outrank methodology defaults too: advise once with a specific risk note, build what they confirm, record it, and don't bring it up again.
  • Injuries and medical concerns: program conservatively around them and refer out per reference/safety.md. Never coach through pain.
  • Only claim the plan is written after athlete/plan.json exists on disk this turn.

Output

Present: a one-line summary grounded in observed data ("Built a 12-week half build around your current 22 mi/wk base and 9:05 easy pace"), the goal verdict, the shape of the build (phases, cutback weeks, peak long run, taper), and the week 1 table. Note that the invariant check passed.

Then suggest 2–3 next actions (skip if you just asked a question): sync recent activities (strava-sync) so grading starts from day one, generate the visual dashboard (dashboard), or set up fueling targets to match the new volume (nutrition-setup). Always close with the cadence line: tomorrow morning, say "good morning" and I'll run your check-in; on Monday, ask "how was my week?" for your first review.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.