{"slug": "claude-coach-bootstrap-skill", "title": "Claude Coach Bootstrap Skill", "summary": "A developer created a skill called 'Claude Coach Bootstrap' that interviews users and scaffolds a personalized git-based repository where an LLM acts as an ongoing strength-and-conditioning coach. The system stores all state in files rather than the model's context, enabling long-running per-session tracking across multiple sessions without memory. The skill guides users through an interview process to determine goals, schedule, and preferred structure level, then generates a repo with schemas, validators, and CI to maintain consistency.", "body_md": "| name | coach-repo-bootstrap |\n|---|---|\n| description | Interview a user and scaffold a personalized git-based repo where an LLM acts as their ongoing strength-and-conditioning coach, with all state kept in files rather than the model's context. Use this whenever someone wants to set up a workout or training tracker, a personal-coach or AI-fitness-log repo, an RPE-autoregulated strength program, or any long-running per-session tracking system an agent maintains across sessions — even if they never say \"skill,\" \"scaffold,\" or \"repo,\" and even if they only describe the outcome (\"I want an AI to plan and log my workouts and handle the progression for me\"). |\n\nInterview the user, then scaffold a git repo where an LLM coaches them across sessions with all\nstate in files. Work in order — **interview (§1), scaffold (§2), verify (§5)** — and don't\nassume their goals, schedule, equipment, or body: a wrong guess baked into the scaffold costs\nmore to undo than a question costs to ask. The Gotchas (§4) encode real failure modes; treat\nthem as load-bearing.\n\nThe coach has no memory between sessions, so continuity lives in the repo, not the model: each session an agent reads a few small files, reasons, writes conclusions back, and forgets. Three rules keep that loop working over months, and they drive every choice below:\n\n**Small working memory**— the state needed to plan a session fits in a few kilobytes, reloadable from scratch each time; detailed history is archived, not re-read.**Write each fact once**— one canonical home per fact; everything else points to it.** External guardrails**— a schema, a validator, and CI, because the model silently drifts its own conventions over time (see Gotchas).\n\nAsk these in small batches (don't dump all of them at once). Adapt follow-ups to their answers. Your goal is to fill in the templates in Phase 2, so listen for those values.\n\n**A. Goals & context**\n\n- What do you want out of training? (strength, size, endurance, general health, a specific event/deadline, rehab from an injury…)\n- Experience level and background? (never trained / returning / experienced / coming from a specific modality like CrossFit, running, lifting)\n- Anything time-bound? (a race, a trip, a season starting)\n\n**B. How much structure do you want** — *this is the key axis; don't skip it.*\nPrograms live on a spectrum from fully-planned to fully-improvised. Ask where they want to be:\n\n**Fixed**— \"tell me exactly what to do each day.\" A written weekly template with prescribed days. Predictable; brittle if their week is chaotic.**Scaffolded**— \"give me a default week, but adapt when life happens.\" A soft weekly shape plus a library the coach composes from.*Good default for most people.***Flexible**— \"just tell me the best thing to do on the day I show up.\" A library of exercises + a few hard constraints; every session composed on the fly from what's stale. Lowest overhead; needs the staleness tooling to stay coherent.\n\nTell them they can start anywhere and move along the spectrum later (many people start Fixed and loosen once fixed days stop fitting their week). Record their choice; it decides which program blocks you generate in Phase 2.\n\n**C. Schedule reality**\n\n- How many sessions/week can you\n*realistically*hit (not aspirationally)? - Typical session length? Are short 15–25 min sessions acceptable, or only \"real\" workouts?\n- Fixed training days, or does it move around week to week?\n- Any recurring activities that already train something? (a sport, a weekly class, a commute-by-bike). These matter — they can \"cover\" a training goal so the coach doesn't double-program it. Ask what each one demands physically and roughly how hard/long it is.\n- What\n*kinds*of sessions will you log? Start from`strength`\n\nand`cardio`\n\n, then add every named activity they do (yoga, a specific sport, a class, a swim). This answer becomes the log`type`\n\nset and the validator's`VALID_TYPES`\n\n(§3.2) — if you leave it at the generic default, the validator will reject the user's real activities or the coach will silently flatten them into a lossy`sport`\n\n.\n\n**D. Locations & equipment** — one profile per place they train\n\n- Where do you train? (home, a specific gym, outdoors, travel…)\n- For each: what equipment is actually there? Be concrete (dumbbell range and increments, machines, bars, bands, cardio options, floor space).\n- Safety constraints? (no spotter → avoid movements where failure is dangerous; no rack; etc.)\n\n**E. Constraints & health** *(ask gently; all optional; note only what they volunteer)*\n\n- Injuries, past or present, or movements that hurt?\n- Medical conditions or medications that affect training, heart rate, hydration, or recovery? (If they share these, record them so the coach programs around them — but don't press.)\n- Anything you specifically want to avoid or emphasize?\n\n**F. Logging & tracking preferences**\n\n- How much detail do you want in logs — terse, or a full narrative per session?\n- Do you want personal records tracked?\n- Wearables: there's no clean way to auto-link a wearable, so don't build around one. But if\nthey use one, mention that a\n**screenshot** of the wearable's session summary (HR, zones, calories) is easy to drop into a session and read, so they can share those whenever they want the data captured. Treat those numbers as directional, not gospel (see Gotchas).\n\n**G. Progression philosophy** — explain and confirm\n\n- The default is\n**RPE-based autoregulation**: each exercise has a target rep/RPE range and a trigger (\"add weight when all sets hit the top of the range at RPE ≤ 8\"). Load goes up when they clear the bar, holds when they're grinding. No fixed weekly percentages, no calendar deloads — deload by feel when signals stack up. - Briefly teach the RPE scale so their logged numbers mean something:\n`6`\n\n= 4+ reps left ·`7`\n\n= 3 left ·`8`\n\n= 2 left ·`9`\n\n= 1 left ·`10`\n\n= max, nothing left. - Confirm this suits them, or adjust (some people want fixed linear progression instead).\n\nWhen the interview is done, **play back a summary** of what you heard and get confirmation\nbefore scaffolding.\n\n**Before writing any files, build the exercise library from the interview** — the specific\nmovements they'll actually train, from their goals, equipment, and chosen structure level.\nEach library entry carries a `movement_pattern`\n\ntag (a muscle-group/goal like `squat_bilateral`\n\nor `cardio_easy`\n\n); **those tags are the pattern vocabulary the whole repo keys off** — the\nstaleness briefing and the validator both derive it from the library, so there's no separate\npattern list to maintain. Tag only patterns they'll really train (a walker who lifts twice a\nweek has no `cardio_intervals`\n\n), and getting the library from the user rather than this doc's\nexamples is what keeps the scaffold from overfitting.\n\nAlso note: **every concrete example here — yoga covering core, a weekly sport banking interval\ncardio, HR-elevating medication — is an illustration, not a default.** Substitute the user's\nreal activities and constraints and delete anything that doesn't apply to them.\n\nThen create this structure. Fill every template from the interview answers, and delete blocks that don't apply to the structure level they chose (Fixed / Scaffolded / Flexible).\n\n```\ntheir-coach-repo/\n├── CLAUDE.md                 # operating manual for future session-agents (see §2.6)\n├── README.md                 # short human-facing overview\n├── profile.yaml              # who they are, goals, schedule, constraints\n├── equipment/\n│   └── <location>.yaml       # one per training location\n├── program/\n│   └── current.yaml          # the exercise library + program state + pattern coverage (the one source of truth)\n├── logs/\n│   ├── TEMPLATE.yaml         # canonical session-log schema (copy per session)\n│   └── prs.yaml              # personal records, keyed by exercise_id\n├── scripts/\n│   ├── staleness.py          # session-start briefing (derives everything from current.yaml)\n│   └── validate.py           # schema check (also runs in CI)\n└── .github/workflows/validate.yml\n```\n\nInitialize git, make the first commit after everything validates, and (if they use GitHub) push and optionally open it as a repo.\n\n```\n# Athlete Profile\nname: \"<name>\"\ngoals:\n  - \"<their goals, one per line>\"\nexperience: \"<background>\"\n\nschedule:\n  approach: \"<Fixed | Scaffolded | Flexible> - <one-line description of what that means here>\"\n  realistic_frequency: \"<e.g. 2-3 strength + 1 cardio per week>\"\n  session_length: \"<e.g. 20-45 min; short sessions OK>\"\n  recurring:\n    # activities that already train something - the coach credits these (see pattern_coverage in current.yaml)\n    \"<e.g. saturday>\": \"<e.g. yoga class with partner>\"\n\npreferences:\n  - \"<logging: voice or text>\"\n  - \"<how much detail they want>\"\n  - \"<anything else they told you>\"\n\n# Only include if they volunteered health/medical info. Program around it; don't over-share.\nconstraints:\n  injuries: []          # e.g. \"left knee - patellofemoral pain on deep lunges\"\n  medical: []           # conditions / meds that affect training, HR, hydration, recovery\n  avoid: []             # movements or patterns to skip\nname: \"<Home Gym | Anytime Fitness | ...>\"\nlocation: <short-id-matching-log-location>   # e.g. home, gym, travel\n\nstrength:\n  - \"<be specific: dumbbell range + increments, bars, machines, bands, bench, rack...>\"\ncardio:\n  - \"<treadmill, bike, rower, outdoor routes, jump rope...>\"\n\nnotes: |\n  - Safety: <e.g. no spotter - avoid movements where failure is dangerous>\n  - <anything about loading increments, quirks, what's missing>\n```\n\nThis is the source of truth for current working weights and trends. The **exercise library**\nis common to all three structure levels; the scheduling blocks differ.\n\n```\nname: \"<program name>\"\nstart_date: <YYYY-MM-DD>\nphase: \"<free text, e.g. 'Base building (RPE-driven)'>\"\n\n# Progression is autoregulated by RPE trigger per exercise, not by calendar week.\nprogression_plan: |\n  Load goes up when an exercise's `progression` trigger is met (e.g. all working sets at the\n  top of the rep range, RPE <= 8, clean form). Working RPE lives in the 7-9 band. RPE 9 on a\n  top set is fine; RPE 9 on every set means hold, don't add. Deload by feel when signals stack\n  up (multiple lifts stalling, RPE 9-10 everywhere, poor recovery) - no scheduled deload week.\n\n# ==================== EXERCISE LIBRARY ====================\n# The single source of truth for working weights, trends, AND recency. current.last_session is\n# what the staleness script reads to date each movement pattern - so it's written once, here,\n# and never duplicated into a separate tracker. Keep each current.trend to ~2 sentences (last\n# result + next trigger); the full narrative lives in the session log.\nexercises:\n  <exercise_id>:                        # stable snake_case key, e.g. bench_db, squat_goblet\n    name: \"<display name>\"\n    variant: \"<canonical description, e.g. 'Flat, dumbbell'>\"\n    movement_pattern: <pattern_key>     # a muscle-group/goal tag; the set of these tags across\n                                        # the library IS the pattern vocabulary (staleness + validator derive it)\n    role: primary                       # primary | accessory | finisher | fallback\n    target: \"3x6-8 @ RPE 7-8\"\n    progression: \"Add ~5 lb when all sets hit 8 reps at RPE <= 8 with clean control\"\n    notes: \"<cues, substitutions, form flags - optional>\"\n    current:\n      weight: \"<e.g. '45 lb each' or null if not yet trained>\"\n      last_session: null                # YYYY-MM-DD, set after first time trained\n      trend: \"<2 sentences: last result + next trigger. Empty until first session.>\"\n  # ... one block per movement they'll train. Start with a handful; add more as needs surface.\n\n# ==================== CARDIO ====================\n# Cardio doesn't compose with strength lifts, so it lives separately as templates-with-state.\ncardio:\n  <cardio_id>:                          # e.g. zone_2, intervals\n    name: \"<e.g. Easy Cardio (Zone 2)>\"\n    intensity: easy                     # easy | hard\n    movement_pattern: <pattern_key>\n    modality: \"<walk/run, bike, row - whatever's available>\"\n    pacing: \"<e.g. talk test - full sentences>\"\n    progression: \"<e.g. add 5 min every 2 weeks; build duration over speed>\"\n    current: { duration: null, last_session: null, trend: \"\" }\n\n# ==================== SESSION SHAPES ====================\n# Rough sizes the coach composes from - not prescriptive.\nsession_shapes:\n  quick:    \"15-25 min. 1-2 exercises hitting one stale pattern.\"\n  standard: \"25-45 min. 1 primary + 1-2 accessories + optional finisher.\"\n  big:      \"45-60 min. 2 primaries + 2-3 accessories + finisher.\"\n\n# ==================== PATTERN COVERAGE ====================\n# Which OTHER patterns also train a goal when performed. The staleness script uses this to\n# compute EFFECTIVE staleness so the coach doesn't nag about a goal already trained indirectly\n# (e.g. a weekly yoga class covers `core`; a sport banks `cardio_intervals`). This is static\n# config - set it once from the recurring activities in the interview; it needs no per-session\n# updates. Every key and every covered_by entry must be a pattern some library entry tags.\npattern_coverage: {}\n  # Example:\n  # core:\n  #   covered_by: [yoga]\n  #   note: \"Weekly class is core-heavy - core is trained even when a plank isn't programmed.\"\n  # cardio_easy:\n  #   covered_by: [yoga]\n  #   partial: true          # covers the goal but doesn't fully replace a dedicated session\n\n# ==================== SCHEDULING (pick ONE block per the chosen structure level) ====================\n\n# --- FIXED: an explicit weekly template ---\nweekly_template:\n  monday:    { name: \"Lower A\",  exercises: [squat_id, hinge_id, core_id] }\n  wednesday: { name: \"Upper A\",  exercises: [bench_id, row_id, ohp_id] }\n  # ...one entry per training day. The coach follows this unless the user deviates.\n\n# --- SCAFFOLDED: a soft default shape + hard constraints ---\nweekly_scaffold:\n  shape: |\n    A default *shape* for a normal week, not a fixed calendar. When the week is chaotic, fall\n    back to on-the-fly composition (staleness + energy + time). Example:\n      2 lower/upper strength days + 1-2 cardio, arranged around the week's fixed points.\n  constraints:\n    - \"<e.g. no hard legs in the 2 days before the Saturday yoga class>\"\n\n# --- FLEXIBLE: library + constraints only, composed on the fly ---\ncomposition_rules: |\n  No weekly template. Each session: (1) run scripts/staleness.py, (2) ask energy/time,\n  (3) pick 2-4 exercises hitting the stalest patterns that fit the session shape and don't\n  pile onto a beat-up body, (4) propose and confirm before starting.\nconstraints:\n  - \"<hard rules that always hold, e.g. leg-freshness before a balance-heavy class>\"\n\nweekly_targets:\n  strength: \"<e.g. 2 sessions minimum>\"\n  cardio: \"<e.g. 1 easy + 1 hard>\"\n  recovery_floor: \"After 3+ hard days in a row, take a true rest or easy day.\"\n```\n\nOnly keep the scheduling block matching their chosen level. Everything above\n\n`SCHEDULING`\n\nis shared. If they're unsure, useScaffolded— it degrades gracefully in both directions.There is\n\nno separate recency tracker file. Pattern staleness is derived from each library entry's`current.last_session`\n\n, and`pattern_coverage`\n\nlives in this same file — so a session only ever updates`current.yaml`\n\n(and`prs.yaml`\n\n), never a hand-pruned session list.\n\n```\n# Session Log TEMPLATE. Copy per session: logs/YYYY-MM-DD.yaml\n# (add a -slug suffix for a 2nd session same day: logs/YYYY-MM-DD-ohp.yaml).\n# Keep field NAMES verbatim so logs stay parseable. Run scripts/validate.py after writing.\n#\n# Required: date, day_of_week, type, location. Strength sessions also need `exercises:`;\n# non-strength sessions need a narrative `notes:` block.\n\ndate: 2026-01-01           # ISO date in the USER'S timezone (see Gotchas - don't use UTC)\nday_of_week: Wednesday     # look up: TZ=\"<their/timezone>\" date -d 'YYYY-MM-DD' '+%A'\ntype: strength             # strength | cardio | yoga | sport | rest  (customize the set)\nlocation: home             # matches an equipment/ profile id, or a free label\nduration: \"~30 min\"        # single canonical field (NOT duration_min / duration_estimate)\nfocus: \"Short description of the session's intent\"\nenergy: moderate           # low | moderate | high (optional)\n\nnotes: |                   # ONE narrative field (not session_context/session_notes/how_felt)\n  Why composed this way, anything notable, pain/form flags, recovery context, what's next.\n\n# Strength sessions:\nexercises:\n  - name: Bench Press\n    exercise_id: bench_db          # REQUIRED - a program/current.yaml key (the stable match\n                                   #   key), or null for a genuine one-off not in the library\n    variant: \"Flat, dumbbell\"      # REQUIRED - exactly how it was done today (free text)\n    movement_pattern: horizontal_push   # must match a movement_pattern tag used in program/current.yaml\n    sets:\n      - { reps: 8, weight: \"45 lb each\", rpe: 8 }\n    notes: \"How it compared to last time; progression-gate status; what to load next.\"\n\n# Non-strength sessions: use a narrative notes: block above and optionally:\n# session:      { name, start_time, end_time, source, rpe }\n# heart_rate:   { avg_bpm, calories, zone_distribution, summary }   # directional only\n# recovery_context: { previous_strength, previous_legs_loaded, notes }\n\n# PRs hit this session. ALWAYS a list of objects (never bare strings). Mirror into prs.yaml.\nprs:\n  - { exercise: \"Bench Press\", variant: \"Flat, dumbbell\", type: rep_pr, detail: \"8 @ 45 lb (up from 40)\" }\n# Personal records, keyed by exercise_id (a program/current.yaml key). Retired movements use\n# a legacy_* key. Each entry's notes carry the specific variant/implement.\nrecords: {}\n  # Example:\n  # bench_db:\n  #   rep_pr:\n  #     - { reps: 8, weight: \"45 lb each\", date: \"2026-05-24\", notes: \"up from 7 on 5/14\" }\n  #   volume_pr:\n  #     - { sets: 3, reps: \"8/8/9\", weight: \"45 lb each\", date: \"2026-05-24\" }\n```\n\nGenerate a CLAUDE.md so every future session an agent knows how to run. It should contain, customized to this user:\n\n**Role & timezone.**\"You are this person's strength coach. Their timezone is`<tz>`\n\n— the system date may be UTC; convert before naming log files. Look up day-of-week with`TZ=\"<tz>\" date -d 'YYYY-MM-DD' '+%A'`\n\n, don't guess.\"**Core principles.** Progressive overload; RPE autoregulation; equipment-awareness; track variants not false equivalences; recovery matters; compose per the chosen structure level.**Session workflow — starting.** Run`python3 scripts/staleness.py`\n\nfirst (never hand-compute staleness). Read the relevant`equipment/`\n\nprofile and`program/current.yaml`\n\n. Ask energy/time. Propose 2-4 exercises (or one cardio) and confirm before starting.**Session workflow — during.** Confirm any ambiguous numbers back before recording; ask RPE if omitted; note pain/form issues. (If they paste a wearable screenshot, read HR/zones/ calories off it into the log; treat those as directional.)**Session workflow — after.**(a) Copy`logs/TEMPLATE.yaml`\n\n→`logs/YYYY-MM-DD.yaml`\n\n. (b) Update each trained exercise's`current`\n\nin`program/current.yaml`\n\n(weight/duration,`last_session`\n\n= today, 2-sentence trend; bump`target`\n\nif the trigger was met). This is the only recency bookkeeping — the staleness script reads`last_session`\n\n, so there is no separate tracker to update. (c) Update`logs/prs.yaml`\n\nfor any PRs. (d) Run`python3 scripts/validate.py`\n\nand fix errors. (e) Commit; push; open a PR titled`Workout: <focus> - <date>`\n\nwith a summary.**The write-once rule.** Full narrative → the log. 2-sentence state + next trigger →`current.trend`\n\n. Recency is*derived*from`current.last_session`\n\n, not written anywhere separately. Never paste the same paragraph into two files.**Notation standards**— restate the units and`exercise_id`\n\n/`variant`\n\nrules from Gotchas 4–5 here, but addressed to the*session-agent*(not the builder), as the rule + one example, not the Gotchas commentary. This is the one deliberate exception to write-once: CLAUDE.md is the manual the session-agent actually reads each time, so it needs its own local copy of the conventions rather than a pointer into this build spec.**RPE scale** and any**health constraints** to program around.\n\nKeep it tight and imperative. It's read every session — it's part of the working memory.\n\nDrop in `scripts/staleness.py`\n\nand `scripts/validate.py`\n\n(§3), and the CI workflow:\n\n```\n# .github/workflows/validate.yml\nname: Validate logs\non: { push: { branches: [main] }, pull_request: {} }\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with: { python-version: \"3.12\" }\n      - run: pip install pyyaml\n      - run: python3 scripts/validate.py\n```\n\nBoth are plain Python + PyYAML (`pip install pyyaml`\n\n). They read the files you generated.\nCustomize the config constants at the top of `validate.py`\n\nbefore shipping: set `VALID_TYPES`\n\nfrom the session-types answer in Phase 1 (§1 C) — leaving the default five will reject the\nuser's real activities — and set `GRANDFATHER_DATE`\n\nto the repo's start date. Note the\nvalidator enforces the *names and shapes* of required and PR fields, not the presence of every\noptional field (e.g. it flags legacy `duration_min`\n\nbut doesn't require `duration`\n\nitself), so\nit's a drift guard, not a completeness checker.\n\n``` bash\n#!/usr/bin/env python3\n\"\"\"Session-start briefing: days since each movement pattern (muscle group / goal) was trained,\nplus current working weights and next triggers. Everything is derived from program/current.yaml\n- there is no separate tracker. A pattern's last-trained date is the most recent\ncurrent.last_session among the library entries tagged with it (written once, per exercise).\npattern_coverage (also in current.yaml) credits patterns that other session types train, so\nEFFECTIVE staleness = the most recent of a pattern's direct date and any covering pattern's\ndate. Run: python3 scripts/staleness.py\"\"\"\n\nfrom datetime import date, datetime\nfrom pathlib import Path\nimport yaml\n\nREPO = Path(__file__).resolve().parent.parent\n\ndef to_date(v):\n    if v is None:\n        return None\n    if isinstance(v, date):\n        return v\n    return datetime.strptime(str(v), \"%Y-%m-%d\").date()\n\ndef main():\n    today = date.today()\n    program = yaml.safe_load((REPO / \"program\" / \"current.yaml\").read_text())\n    coverage = program.get(\"pattern_coverage\", {}) or {}\n\n    # Group library entries by pattern; each pattern's direct date is the newest last_session\n    # among its entries. The set of patterns is defined by the tags in the library itself.\n    by_pattern, direct = {}, {}\n    for kind in (\"exercises\", \"cardio\"):\n        for ex in (program.get(kind) or {}).values():\n            last = to_date((ex.get(\"current\") or {}).get(\"last_session\"))\n            pats = ex.get(\"movement_pattern\") or []\n            for p in ([pats] if isinstance(pats, str) else pats):\n                by_pattern.setdefault(p, []).append(ex)\n                if last and (direct.get(p) is None or last > direct[p]):\n                    direct[p] = last\n\n    effective = {}\n    for p in by_pattern:\n        best, via = direct.get(p), None\n        for coverer in coverage.get(p, {}).get(\"covered_by\", []):\n            cov = direct.get(coverer)\n            if cov and (best is None or cov > best):\n                best, via = cov, coverer\n        effective[p] = (best, via)\n\n    rows = sorted(by_pattern, key=lambda p: (effective[p][0] is not None, effective[p][0] or date.min))\n\n    print(f\"Movement pattern staleness as of {today} ({today.strftime('%A')})\")\n    print(\"(EFF credits pattern_coverage; '~' = partial coverage)\\n\")\n    print(f\"{'PATTERN':<20} {'DIRECT':<12} {'DAYS':>5}  {'EFF':>5}  COVERED BY\")\n    print(\"-\" * 62)\n    for p in rows:\n        d = direct.get(p)\n        eff_date, via = effective[p]\n        days = \"never\" if d is None else str((today - d).days)\n        eff_days = \"never\" if eff_date is None else str((today - eff_date).days)\n        partial = \"~\" if via and coverage.get(p, {}).get(\"partial\") else \"\"\n        via_str = f\"{partial}{via} ({eff_date})\" if via else \"\"\n        print(f\"{p:<20} {str(d or '-'):<12} {days:>5}  {eff_days:>5}  {via_str}\")\n\n    print(\"\\nCurrent state per pattern (stalest first):\\n\")\n    for p in rows:\n        eff_date, via = effective[p]\n        header = \"never trained\" if eff_date is None else f\"{(today - eff_date).days} days ago\" + (f\" via {via}\" if via else \"\")\n        print(f\"## {p} ({header})\")\n        for ex in by_pattern[p]:\n            cur = ex.get(\"current\") or {}\n            load = cur.get(\"weight\") or cur.get(\"duration\") or \"?\"\n            role = f\" [{ex['role']}]\" if ex.get(\"role\") else \"\"\n            print(f\"  {ex['name']} ({ex.get('variant', '')}){role}: {load}\")\n            trend = (cur.get(\"trend\") or \"\").strip().replace(\"\\n\", \" \")\n            if trend:\n                print(f\"    trend: {trend}\")\n        print()\n\nif __name__ == \"__main__\":\n    main()\nbash\n#!/usr/bin/env python3\n\"\"\"Validate session logs and program state against the canonical schema. The movement-pattern\nvocabulary is defined by the tags on program/current.yaml library entries - there is no\nseparate tracker file to keep in sync. Logs dated before GRANDFATHER_DATE warn only (they\npredate later schema changes); newer logs must pass. Exits nonzero on any error. Run in CI and\nafter writing a log.\"\"\"\n\nimport re, sys\nfrom datetime import date, datetime\nfrom pathlib import Path\nimport yaml\n\nREPO = Path(__file__).resolve().parent.parent\n\n# --- CUSTOMIZE THESE for the user ---\nGRANDFATHER_DATE = date(2000, 1, 1)   # set to the repo start date; bump when you migrate schema\nVALID_TYPES = {\"strength\", \"cardio\", \"yoga\", \"sport\", \"rest\"}\n# ------------------------------------\n\nREQUIRED_FIELDS = [\"date\", \"day_of_week\", \"type\", \"location\"]\nLEGACY_FIELDS = {  # names the model tends to drift into -> the canonical name to use instead\n    \"day\": \"day_of_week\", \"duration_min\": \"duration\", \"duration_estimate\": \"duration\",\n    \"session_context\": \"notes\", \"session_notes\": \"notes\", \"how_user_felt\": \"notes\",\n}\nPR_KEYS = {\"exercise\", \"variant\", \"type\", \"detail\"}\nerrors, warnings = [], []\n\ndef to_date(v):\n    if isinstance(v, date):\n        return v\n    try:\n        return datetime.strptime(str(v), \"%Y-%m-%d\").date()\n    except (ValueError, TypeError):\n        return None\n\ndef report(path, msg, log_date):\n    (warnings if (log_date and log_date < GRANDFATHER_DATE) else errors).append(f\"{path.relative_to(REPO)}: {msg}\")\n\ndef load_program():\n    return yaml.safe_load((REPO / \"program\" / \"current.yaml\").read_text())\n\ndef known_patterns(program):\n    \"\"\"The pattern vocabulary = every movement_pattern tag used by a library entry.\"\"\"\n    pats = set()\n    for kind in (\"exercises\", \"cardio\"):\n        for ex in (program.get(kind) or {}).values():\n            p = ex.get(\"movement_pattern\")\n            for tag in (p if isinstance(p, list) else [p]):\n                if tag:\n                    pats.add(tag)\n    return pats\n\ndef library_ids(program):\n    ids = set()\n    for kind in (\"exercises\", \"cardio\"):\n        ids |= set((program.get(kind) or {}).keys())\n    return ids\n\ndef check_log(path, patterns, lib):\n    try:\n        docs = list(yaml.safe_load_all(path.read_text()))\n    except yaml.YAMLError as e:\n        errors.append(f\"{path.relative_to(REPO)}: YAML parse error: {e}\")\n        return\n    m = re.match(r\"(\\d{4}-\\d{2}-\\d{2})\", path.name)\n    fname_date = to_date(m.group(1)) if m else None\n    if len(docs) > 1:\n        report(path, f\"{len(docs)} YAML docs in one file - one session per file\", fname_date)\n    doc = docs[0] if docs else None\n    if not isinstance(doc, dict):\n        report(path, \"log is not a YAML mapping\", fname_date)\n        return\n    log_date = to_date(doc.get(\"date\")) or fname_date\n    for f in REQUIRED_FIELDS:\n        if f not in doc:\n            report(path, f\"missing required field '{f}'\", log_date)\n    if fname_date and to_date(doc.get(\"date\")) not in (None, fname_date):\n        report(path, f\"date {doc.get('date')} != filename date {fname_date}\", log_date)\n    if log_date and doc.get(\"day_of_week\"):\n        actual = log_date.strftime(\"%A\")\n        if str(doc[\"day_of_week\"]) != actual:\n            report(path, f\"day_of_week '{doc['day_of_week']}' should be '{actual}'\", log_date)\n    for legacy, canon in LEGACY_FIELDS.items():\n        if legacy in doc:\n            report(path, f\"legacy field '{legacy}' - use '{canon}'\", log_date)\n    stype = doc.get(\"type\")\n    if stype and stype not in VALID_TYPES:\n        report(path, f\"unknown type '{stype}' (expected {sorted(VALID_TYPES)})\", log_date)\n    if stype == \"strength\":\n        exs = doc.get(\"exercises\")\n        if not isinstance(exs, list) or not exs:\n            report(path, \"strength session needs a non-empty 'exercises' list\", log_date)\n        else:\n            for i, ex in enumerate(exs):\n                lbl = f\"exercises[{i}]\"\n                if not isinstance(ex, dict):\n                    report(path, f\"{lbl} is not a mapping\", log_date); continue\n                for f in (\"name\", \"variant\", \"movement_pattern\"):\n                    if not ex.get(f):\n                        report(path, f\"{lbl} ({ex.get('name','?')}) missing '{f}'\", log_date)\n                if \"exercise_id\" not in ex:\n                    report(path, f\"{lbl} missing 'exercise_id' (a program key, or null for a one-off)\", log_date)\n                elif ex[\"exercise_id\"] is not None and ex[\"exercise_id\"] not in lib:\n                    report(path, f\"{lbl} exercise_id '{ex['exercise_id']}' not a program/current.yaml key\", log_date)\n                pat = ex.get(\"movement_pattern\")\n                for p in (pat if isinstance(pat, list) else [pat]):\n                    if p and p not in patterns:\n                        report(path, f\"{lbl} movement_pattern '{p}' is not tagged by any library entry\", log_date)\n                if \"sets\" not in ex:\n                    report(path, f\"{lbl} ({ex.get('name','?')}) missing 'sets'\", log_date)\n    elif stype in VALID_TYPES and not doc.get(\"notes\") and not doc.get(\"exercises\"):\n        report(path, f\"{stype} session needs a narrative 'notes' block\", log_date)\n    prs = doc.get(\"prs\")\n    if prs is not None:\n        if not isinstance(prs, list):\n            report(path, \"'prs' must be a list of objects\", log_date)\n        else:\n            for i, pr in enumerate(prs):\n                if not isinstance(pr, dict):\n                    report(path, f\"prs[{i}] must be an object\", log_date)\n                elif not PR_KEYS.issubset(pr):\n                    report(path, f\"prs[{i}] missing keys: {sorted(PR_KEYS - set(pr))}\", log_date)\n\ndef check_program(program, patterns):\n    # last_session values must parse, and pattern_coverage must reference real patterns.\n    for kind in (\"exercises\", \"cardio\"):\n        for eid, ex in (program.get(kind) or {}).items():\n            ls = (ex.get(\"current\") or {}).get(\"last_session\")\n            if ls is not None and to_date(ls) is None:\n                errors.append(f\"program/current.yaml: {kind}.{eid} current.last_session invalid date '{ls}'\")\n    for k, cov in (program.get(\"pattern_coverage\") or {}).items():\n        if k not in patterns:\n            errors.append(f\"program/current.yaml: pattern_coverage key '{k}' has no library entry that trains it\")\n        for c in (cov or {}).get(\"covered_by\", []):\n            if c not in patterns:\n                errors.append(f\"program/current.yaml: pattern_coverage.{k} covered_by '{c}' has no library entry\")\n\ndef check_prs_file(lib):\n    data = yaml.safe_load((REPO / \"logs\" / \"prs.yaml\").read_text()) or {}\n    for key in (data.get(\"records\") or {}):\n        if key not in lib and not str(key).startswith(\"legacy_\"):\n            errors.append(f\"logs/prs.yaml: record key '{key}' not a program id or legacy_* key\")\n\ndef main():\n    program = load_program()\n    patterns, lib = known_patterns(program), library_ids(program)\n    logs = sorted((REPO / \"logs\").glob(\"*.yaml\"))\n    skip = {\"TEMPLATE.yaml\", \"prs.yaml\"}\n    for p in logs:\n        if p.name not in skip:\n            check_log(p, patterns, lib)\n    check_program(program, patterns)\n    check_prs_file(lib)\n    for w in warnings:\n        print(f\"WARN  {w}\")\n    for e in errors:\n        print(f\"ERROR {e}\")\n    print(f\"\\n{len(logs) - len(skip)} logs checked: {len(errors)} error(s), {len(warnings)} warning(s)\")\n    sys.exit(1 if errors else 0)\n\nif __name__ == \"__main__\":\n    main()\n```\n\n-\n**The model will silently corrupt its own conventions.** Across a long-running repo, an LLM drifts: the same field becomes`day`\n\nthen`day_of_week`\n\n; durations become`duration_min`\n\n/`duration_estimate`\n\n; PRs become bare strings. Each choice looks fine in isolation; the aggregate rots the store you depend on.**You cannot fix this with willpower**— the next session's agent doesn't inherit this one's resolve. Fix it with*external*structure: the documented schema (TEMPLATE.yaml), the validator, and CI on every push. The`LEGACY_FIELDS`\n\nmap in validate.py exists specifically to catch this drift. -\n**Write each fact exactly once.** Two homes: full narrative → the session log; 2-sentence state + next trigger →`current.trend`\n\n. Recency isn't a third home — it's*derived*from`current.last_session`\n\n, which is why there's no separate tracker file to hand-maintain. Duplicating a paragraph across files means future updates skew and the working memory bloats; if you're pasting, point to the log instead. (This is exactly why`recent.yaml`\n\nwas removed: it re-stored last-trained dates that already live in`current.yaml`\n\n.) -\n**Never hand-compute staleness.** Run`staleness.py`\n\n; trust it. An LLM eyeballing \"days since\" will produce confident wrong numbers. Don't write \"17 days stale\" into any file — it rots; compute it on demand. -\n**Stable** The`exercise_id`\n\nvs free-text`variant`\n\n.`exercise_id`\n\n(a library key) is the*matching key*— PR and history tracking key off it, so it must be exact. The`variant`\n\nis free text for how it was done today. This split lets you swap equipment (a dumbbell bench for a machine press) or migrate implements without breaking history or looking like a regression. Same movement, changed conditions → same id, new variant, note it. A genuinely new movement gets its own id. -\n**Units are the disambiguator; always carry them.**`2x30`\n\nis ambiguous — two 30 lb dumbbells, or 2 sets of 30? Rule:**weights always carry a unit**(`2×30 lb`\n\n= two 30s),**sets×reps never do**(`3×8`\n\n= 3 sets of 8). So`3×8 @ 2×30 lb`\n\nis unambiguous. Put this in CLAUDE.md. -\n**Timezone the log filenames.** The system clock may be UTC; the user isn't. Name log files and set`date:`\n\nin the user's timezone, and look up`day_of_week`\n\nwith`TZ=\"<their/tz>\" date -d 'YYYY-MM-DD' '+%A'`\n\nrather than guessing (the validator checks that the day matches the date). -\n**Confirm dictated numbers back.** Voice logging is great but speech-to-text garbles numbers (\"fifty\" vs \"fifteen\", \"45 for 3 sets of 8\"). Read the numbers back before writing. -\n**Match the structure level they chose — and let it evolve.** Don't impose a rigid weekly template on someone with a chaotic schedule, and don't leave a beginner who wants to be told what to do staring at an empty library. Whatever you pick isn't permanent — revisit it as their schedule and adherence show what actually fits. -\n**Effective staleness / coverage stops the coach nagging.** If a recurring activity trains a goal (a weekly class covers`core`\n\n; a sport banks interval cardio), record it in`pattern_coverage`\n\nso the goal doesn't read as \"stale\" when it's actually being trained indirectly. Re-examine coverage when the user's routine changes (e.g. a sport goes off-season — its coverage should stop counting). -\n**Sensor data is directional, not gospel.** Wearable HR zones are formula-estimated and can be skewed by meds, heat, stress, caffeine. Log the numbers for the record, but pace by feel (talk test for easy work, RPE for hard) and trust the user over the watch when they disagree. Don't build progression off a single HR reading. -\n**Grandfather your schema migrations.** When you change the schema mid-life, don't retro- break every old log. Set`GRANDFATHER_DATE`\n\nso pre-migration logs*warn*and post-migration logs*error*. Migrate the back-catalog deliberately in its own commit, not all at once under pressure.\n\nBefore you hand the repo over, verify:\n\n- Interview summary was played back and confirmed by the user.\n- The exercise library was built from the interview, not kept as the doc's example list,\nand every\n`movement_pattern`\n\ntag reflects a pattern the user will actually train. - Every\n`exercise_id`\n\nyou'll log against exists as a key in`program/current.yaml`\n\n. - Every\n`pattern_coverage`\n\nkey and`covered_by`\n\nentry is a pattern some library entry tags. -\n`VALID_TYPES`\n\nin`validate.py`\n\nmatches the session types from the interview (§1 C). - Only the scheduling block for their chosen structure level remains in the program.\n-\n`pip install pyyaml && python3 scripts/staleness.py`\n\nruns and prints a sane briefing (everything \"never trained\" on day one is expected). -\n`python3 scripts/validate.py`\n\nexits 0 (no logs yet — it's confirming`current.yaml`\n\nand`prs.yaml`\n\nare internally consistent). - CLAUDE.md covers everything in §2.6.\n-\n`git init`\n\n, first commit, and (if using GitHub) the CI workflow is present so the validator runs on push. - Tell the user how to start a session:\n*open an agent in the repo and say \"I'm training at*`<location>`\n\ntoday, I've got`<time>`\n\n, feeling`<energy>`\n\n— what should I do?\"\n\nThat's it. From here on, the repo is the memory and each session is: brief → compose → confirm → train → log → validate → commit. The coach forgets; the files remember.", "url": "https://wpnews.pro/news/claude-coach-bootstrap-skill", "canonical_source": "https://gist.github.com/brendanlong/1d86d44963327bc00fb5d845f04770ab", "published_at": "2026-07-06 22:44:42+00:00", "updated_at": "2026-07-09 02:11:02.905164+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools"], "entities": ["Claude Coach Bootstrap"], "alternates": {"html": "https://wpnews.pro/news/claude-coach-bootstrap-skill", "markdown": "https://wpnews.pro/news/claude-coach-bootstrap-skill.md", "text": "https://wpnews.pro/news/claude-coach-bootstrap-skill.txt", "jsonld": "https://wpnews.pro/news/claude-coach-bootstrap-skill.jsonld"}}