| name | coach-repo-bootstrap |
|---|---|
| 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"). |
Interview the user, then scaffold a git repo where an LLM coaches them across sessions with all state in files. Work in order β interview (Β§1), scaffold (Β§2), verify (Β§5) β and don't assume their goals, schedule, equipment, or body: a wrong guess baked into the scaffold costs more to undo than a question costs to ask. The Gotchas (Β§4) encode real failure modes; treat them as load-bearing.
The 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:
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).
Ask 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.
A. Goals & context
- What do you want out of training? (strength, size, endurance, general health, a specific event/deadline, rehab from an injuryβ¦)
- Experience level and background? (never trained / returning / experienced / coming from a specific modality like CrossFit, running, lifting)
- Anything time-bound? (a race, a trip, a season starting)
B. How much structure do you want β this is the key axis; don't skip it. Programs live on a spectrum from fully-planned to fully-improvised. Ask where they want to be:
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.
Tell 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.
C. Schedule reality
- How many sessions/week can you realisticallyhit (not aspirationally)? - Typical session length? Are short 15β25 min sessions acceptable, or only "real" workouts?
- Fixed training days, or does it move around week to week?
- 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.
- What
kindsof sessions will you log? Start from
strength
andcardio
, then add every named activity they do (yoga, a specific sport, a class, a swim). This answer becomes the logtype
set and the validator'sVALID_TYPES
(Β§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 lossysport
.
D. Locations & equipment β one profile per place they train
- Where do you train? (home, a specific gym, outdoors, travelβ¦)
- For each: what equipment is actually there? Be concrete (dumbbell range and increments, machines, bars, bands, cardio options, floor space).
- Safety constraints? (no spotter β avoid movements where failure is dangerous; no rack; etc.)
E. Constraints & health (ask gently; all optional; note only what they volunteer)
- Injuries, past or present, or movements that hurt?
- 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.)
- Anything you specifically want to avoid or emphasize?
F. Logging & tracking preferences
- How much detail do you want in logs β terse, or a full narrative per session?
- Do you want personal records tracked?
- Wearables: there's no clean way to auto-link a wearable, so don't build around one. But if they use one, mention that a 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).
G. Progression philosophy β explain and confirm
- The default is
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:
6
= 4+ reps left Β·7
= 3 left Β·8
= 2 left Β·9
= 1 left Β·10
= max, nothing left. - Confirm this suits them, or adjust (some people want fixed linear progression instead).
When the interview is done, play back a summary of what you heard and get confirmation before scaffolding.
Before writing any files, build the exercise library from the interview β the specific
movements they'll actually train, from their goals, equipment, and chosen structure level.
Each library entry carries a movement_pattern
tag (a muscle-group/goal like squat_bilateral
or cardio_easy
); those tags are the pattern vocabulary the whole repo keys off β the
staleness briefing and the validator both derive it from the library, so there's no separate
pattern list to maintain. Tag only patterns they'll really train (a walker who lifts twice a
week has no cardio_intervals
), and getting the library from the user rather than this doc's examples is what keeps the scaffold from overfitting.
Also note: every concrete example here β yoga covering core, a weekly sport banking interval cardio, HR-elevating medication β is an illustration, not a default. Substitute the user's real activities and constraints and delete anything that doesn't apply to them.
Then 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).
their-coach-repo/
βββ CLAUDE.md # operating manual for future session-agents (see Β§2.6)
βββ README.md # short human-facing overview
βββ profile.yaml # who they are, goals, schedule, constraints
βββ equipment/
β βββ <location>.yaml # one per training location
βββ program/
β βββ current.yaml # the exercise library + program state + pattern coverage (the one source of truth)
βββ logs/
β βββ TEMPLATE.yaml # canonical session-log schema (copy per session)
β βββ prs.yaml # personal records, keyed by exercise_id
βββ scripts/
β βββ staleness.py # session-start briefing (derives everything from current.yaml)
β βββ validate.py # schema check (also runs in CI)
βββ .github/workflows/validate.yml
Initialize git, make the first commit after everything validates, and (if they use GitHub) push and optionally open it as a repo.
name: "<name>"
goals:
- "<their goals, one per line>"
experience: "<background>"
schedule:
approach: "<Fixed | Scaffolded | Flexible> - <one-line description of what that means here>"
realistic_frequency: "<e.g. 2-3 strength + 1 cardio per week>"
session_length: "<e.g. 20-45 min; short sessions OK>"
recurring:
"<e.g. saturday>": "<e.g. yoga class with partner>"
preferences:
- "<logging: voice or text>"
- "<how much detail they want>"
- "<anything else they told you>"
constraints:
injuries: [] # e.g. "left knee - patellofemoral pain on deep lunges"
medical: [] # conditions / meds that affect training, HR, hydration, recovery
avoid: [] # movements or patterns to skip
name: "<Home Gym | Anytime Fitness | ...>"
location: <short-id-matching-log-location> # e.g. home, gym, travel
strength:
- "<be specific: dumbbell range + increments, bars, machines, bands, bench, rack...>"
cardio:
- "<treadmill, bike, rower, outdoor routes, jump rope...>"
notes: |
- Safety: <e.g. no spotter - avoid movements where failure is dangerous>
- <anything about increments, quirks, what's missing>
This is the source of truth for current working weights and trends. The exercise library is common to all three structure levels; the scheduling blocks differ.
name: "<program name>"
start_date: <YYYY-MM-DD>
phase: "<free text, e.g. 'Base building (RPE-driven)'>"
progression_plan: |
Load goes up when an exercise's `progression` trigger is met (e.g. all working sets at the
top of the rep range, RPE <= 8, clean form). Working RPE lives in the 7-9 band. RPE 9 on a
top set is fine; RPE 9 on every set means hold, don't add. Deload by feel when signals stack
up (multiple lifts stalling, RPE 9-10 everywhere, poor recovery) - no scheduled deload week.
exercises:
<exercise_id>: # stable snake_case key, e.g. bench_db, squat_goblet
name: "<display name>"
variant: "<canonical description, e.g. 'Flat, dumbbell'>"
movement_pattern: <pattern_key> # a muscle-group/goal tag; the set of these tags across
role: primary # primary | accessory | finisher | fallback
target: "3x6-8 @ RPE 7-8"
progression: "Add ~5 lb when all sets hit 8 reps at RPE <= 8 with clean control"
notes: "<cues, substitutions, form flags - optional>"
current:
weight: "<e.g. '45 lb each' or null if not yet trained>"
last_session: null # YYYY-MM-DD, set after first time trained
trend: "<2 sentences: last result + next trigger. Empty until first session.>"
cardio:
<cardio_id>: # e.g. zone_2, intervals
name: "<e.g. Easy Cardio (Zone 2)>"
intensity: easy # easy | hard
movement_pattern: <pattern_key>
modality: "<walk/run, bike, row - whatever's available>"
pacing: "<e.g. talk test - full sentences>"
progression: "<e.g. add 5 min every 2 weeks; build duration over speed>"
current: { duration: null, last_session: null, trend: "" }
session_shapes:
quick: "15-25 min. 1-2 exercises hitting one stale pattern."
standard: "25-45 min. 1 primary + 1-2 accessories + optional finisher."
big: "45-60 min. 2 primaries + 2-3 accessories + finisher."
pattern_coverage: {}
weekly_template:
monday: { name: "Lower A", exercises: [squat_id, hinge_id, core_id] }
wednesday: { name: "Upper A", exercises: [bench_id, row_id, ohp_id] }
weekly_scaffold:
shape: |
A default *shape* for a normal week, not a fixed calendar. When the week is chaotic, fall
back to on-the-fly composition (staleness + energy + time). Example:
2 lower/upper strength days + 1-2 cardio, arranged around the week's fixed points.
constraints:
- "<e.g. no hard legs in the 2 days before the Saturday yoga class>"
composition_rules: |
No weekly template. Each session: (1) run scripts/staleness.py, (2) ask energy/time,
(3) pick 2-4 exercises hitting the stalest patterns that fit the session shape and don't
pile onto a beat-up body, (4) propose and confirm before starting.
constraints:
- "<hard rules that always hold, e.g. leg-freshness before a balance-heavy class>"
weekly_targets:
strength: "<e.g. 2 sessions minimum>"
cardio: "<e.g. 1 easy + 1 hard>"
recovery_floor: "After 3+ hard days in a row, take a true rest or easy day."
Only keep the scheduling block matching their chosen level. Everything above
SCHEDULING
is shared. If they're unsure, useScaffoldedβ it degrades gracefully in both directions.There is
no separate recency tracker file. Pattern staleness is derived from each library entry'scurrent.last_session
, andpattern_coverage
lives in this same file β so a session only ever updatescurrent.yaml
(andprs.yaml
), never a hand-pruned session list.
#
date: 2026-01-01 # ISO date in the USER'S timezone (see Gotchas - don't use UTC)
day_of_week: Wednesday # look up: TZ="<their/timezone>" date -d 'YYYY-MM-DD' '+%A'
type: strength # strength | cardio | yoga | sport | rest (customize the set)
location: home # matches an equipment/ profile id, or a free label
duration: "~30 min" # single canonical field (NOT duration_min / duration_estimate)
focus: "Short description of the session's intent"
energy: moderate # low | moderate | high (optional)
notes: | # ONE narrative field (not session_context/session_notes/how_felt)
Why composed this way, anything notable, pain/form flags, recovery context, what's next.
exercises:
- name: Bench Press
exercise_id: bench_db # REQUIRED - a program/current.yaml key (the stable match
variant: "Flat, dumbbell" # REQUIRED - exactly how it was done today (free text)
movement_pattern: horizontal_push # must match a movement_pattern tag used in program/current.yaml
sets:
- { reps: 8, weight: "45 lb each", rpe: 8 }
notes: "How it compared to last time; progression-gate status; what to load next."
prs:
- { exercise: "Bench Press", variant: "Flat, dumbbell", type: rep_pr, detail: "8 @ 45 lb (up from 40)" }
records: {}
Generate a CLAUDE.md so every future session an agent knows how to run. It should contain, customized to this user:
Role & timezone."You are this person's strength coach. Their timezone is<tz>
β the system date may be UTC; convert before naming log files. Look up day-of-week withTZ="<tz>" date -d 'YYYY-MM-DD' '+%A'
, 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. Runpython3 scripts/staleness.py
first (never hand-compute staleness). Read the relevantequipment/
profile andprogram/current.yaml
. 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) Copylogs/TEMPLATE.yaml
βlogs/YYYY-MM-DD.yaml
. (b) Update each trained exercise'scurrent
inprogram/current.yaml
(weight/duration,last_session
= today, 2-sentence trend; bumptarget
if the trigger was met). This is the only recency bookkeeping β the staleness script readslast_session
, so there is no separate tracker to update. (c) Updatelogs/prs.yaml
for any PRs. (d) Runpython3 scripts/validate.py
and fix errors. (e) Commit; push; open a PR titledWorkout: <focus> - <date>
with a summary.The write-once rule. Full narrative β the log. 2-sentence state + next trigger βcurrent.trend
. Recency isderivedfromcurrent.last_session
, not written anywhere separately. Never paste the same paragraph into two files.Notation standardsβ restate the units andexercise_id
/variant
rules from Gotchas 4β5 here, but addressed to thesession-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 anyhealth constraints to program around.
Keep it tight and imperative. It's read every session β it's part of the working memory.
Drop in scripts/staleness.py
and scripts/validate.py
(Β§3), and the CI workflow:
name: Validate logs
on: { push: { branches: [main] }, pull_request: {} }
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install pyyaml
- run: python3 scripts/validate.py
Both are plain Python + PyYAML (pip install pyyaml
). They read the files you generated.
Customize the config constants at the top of validate.py
before shipping: set VALID_TYPES
from the session-types answer in Phase 1 (Β§1 C) β leaving the default five will reject the
user's real activities β and set GRANDFATHER_DATE
to the repo's start date. Note the
validator enforces the names and shapes of required and PR fields, not the presence of every
optional field (e.g. it flags legacy duration_min
but doesn't require duration
itself), so it's a drift guard, not a completeness checker.
#!/usr/bin/env python3
"""Session-start briefing: days since each movement pattern (muscle group / goal) was trained,
plus current working weights and next triggers. Everything is derived from program/current.yaml
- there is no separate tracker. A pattern's last-trained date is the most recent
current.last_session among the library entries tagged with it (written once, per exercise).
pattern_coverage (also in current.yaml) credits patterns that other session types train, so
EFFECTIVE staleness = the most recent of a pattern's direct date and any covering pattern's
date. Run: python3 scripts/staleness.py"""
from datetime import date, datetime
from pathlib import Path
import yaml
REPO = Path(__file__).resolve().parent.parent
def to_date(v):
if v is None:
return None
if isinstance(v, date):
return v
return datetime.strptime(str(v), "%Y-%m-%d").date()
def main():
today = date.today()
program = yaml.safe_load((REPO / "program" / "current.yaml").read_text())
coverage = program.get("pattern_coverage", {}) or {}
by_pattern, direct = {}, {}
for kind in ("exercises", "cardio"):
for ex in (program.get(kind) or {}).values():
last = to_date((ex.get("current") or {}).get("last_session"))
pats = ex.get("movement_pattern") or []
for p in ([pats] if isinstance(pats, str) else pats):
by_pattern.setdefault(p, []).append(ex)
if last and (direct.get(p) is None or last > direct[p]):
direct[p] = last
effective = {}
for p in by_pattern:
best, via = direct.get(p), None
for coverer in coverage.get(p, {}).get("covered_by", []):
cov = direct.get(coverer)
if cov and (best is None or cov > best):
best, via = cov, coverer
effective[p] = (best, via)
rows = sorted(by_pattern, key=lambda p: (effective[p][0] is not None, effective[p][0] or date.min))
print(f"Movement pattern staleness as of {today} ({today.strftime('%A')})")
print("(EFF credits pattern_coverage; '~' = partial coverage)\n")
print(f"{'PATTERN':<20} {'DIRECT':<12} {'DAYS':>5} {'EFF':>5} COVERED BY")
print("-" * 62)
for p in rows:
d = direct.get(p)
eff_date, via = effective[p]
days = "never" if d is None else str((today - d).days)
eff_days = "never" if eff_date is None else str((today - eff_date).days)
partial = "~" if via and coverage.get(p, {}).get("partial") else ""
via_str = f"{partial}{via} ({eff_date})" if via else ""
print(f"{p:<20} {str(d or '-'):<12} {days:>5} {eff_days:>5} {via_str}")
print("\nCurrent state per pattern (stalest first):\n")
for p in rows:
eff_date, via = effective[p]
header = "never trained" if eff_date is None else f"{(today - eff_date).days} days ago" + (f" via {via}" if via else "")
print(f"## {p} ({header})")
for ex in by_pattern[p]:
cur = ex.get("current") or {}
load = cur.get("weight") or cur.get("duration") or "?"
role = f" [{ex['role']}]" if ex.get("role") else ""
print(f" {ex['name']} ({ex.get('variant', '')}){role}: {load}")
trend = (cur.get("trend") or "").strip().replace("\n", " ")
if trend:
print(f" trend: {trend}")
print()
if __name__ == "__main__":
main()
bash
#!/usr/bin/env python3
"""Validate session logs and program state against the canonical schema. The movement-pattern
vocabulary is defined by the tags on program/current.yaml library entries - there is no
separate tracker file to keep in sync. Logs dated before GRANDFATHER_DATE warn only (they
predate later schema changes); newer logs must pass. Exits nonzero on any error. Run in CI and
after writing a log."""
import re, sys
from datetime import date, datetime
from pathlib import Path
import yaml
REPO = Path(__file__).resolve().parent.parent
GRANDFATHER_DATE = date(2000, 1, 1) # set to the repo start date; bump when you migrate schema
VALID_TYPES = {"strength", "cardio", "yoga", "sport", "rest"}
REQUIRED_FIELDS = ["date", "day_of_week", "type", "location"]
LEGACY_FIELDS = { # names the model tends to drift into -> the canonical name to use instead
"day": "day_of_week", "duration_min": "duration", "duration_estimate": "duration",
"session_context": "notes", "session_notes": "notes", "how_user_felt": "notes",
}
PR_KEYS = {"exercise", "variant", "type", "detail"}
errors, warnings = [], []
def to_date(v):
if isinstance(v, date):
return v
try:
return datetime.strptime(str(v), "%Y-%m-%d").date()
except (ValueError, TypeError):
return None
def report(path, msg, log_date):
(warnings if (log_date and log_date < GRANDFATHER_DATE) else errors).append(f"{path.relative_to(REPO)}: {msg}")
def load_program():
return yaml.safe_load((REPO / "program" / "current.yaml").read_text())
def known_patterns(program):
"""The pattern vocabulary = every movement_pattern tag used by a library entry."""
pats = set()
for kind in ("exercises", "cardio"):
for ex in (program.get(kind) or {}).values():
p = ex.get("movement_pattern")
for tag in (p if isinstance(p, list) else [p]):
if tag:
pats.add(tag)
return pats
def library_ids(program):
ids = set()
for kind in ("exercises", "cardio"):
ids |= set((program.get(kind) or {}).keys())
return ids
def check_log(path, patterns, lib):
try:
docs = list(yaml.safe_load_all(path.read_text()))
except yaml.YAMLError as e:
errors.append(f"{path.relative_to(REPO)}: YAML parse error: {e}")
return
m = re.match(r"(\d{4}-\d{2}-\d{2})", path.name)
fname_date = to_date(m.group(1)) if m else None
if len(docs) > 1:
report(path, f"{len(docs)} YAML docs in one file - one session per file", fname_date)
doc = docs[0] if docs else None
if not isinstance(doc, dict):
report(path, "log is not a YAML mapping", fname_date)
return
log_date = to_date(doc.get("date")) or fname_date
for f in REQUIRED_FIELDS:
if f not in doc:
report(path, f"missing required field '{f}'", log_date)
if fname_date and to_date(doc.get("date")) not in (None, fname_date):
report(path, f"date {doc.get('date')} != filename date {fname_date}", log_date)
if log_date and doc.get("day_of_week"):
actual = log_date.strftime("%A")
if str(doc["day_of_week"]) != actual:
report(path, f"day_of_week '{doc['day_of_week']}' should be '{actual}'", log_date)
for legacy, canon in LEGACY_FIELDS.items():
if legacy in doc:
report(path, f"legacy field '{legacy}' - use '{canon}'", log_date)
stype = doc.get("type")
if stype and stype not in VALID_TYPES:
report(path, f"unknown type '{stype}' (expected {sorted(VALID_TYPES)})", log_date)
if stype == "strength":
exs = doc.get("exercises")
if not isinstance(exs, list) or not exs:
report(path, "strength session needs a non-empty 'exercises' list", log_date)
else:
for i, ex in enumerate(exs):
lbl = f"exercises[{i}]"
if not isinstance(ex, dict):
report(path, f"{lbl} is not a mapping", log_date); continue
for f in ("name", "variant", "movement_pattern"):
if not ex.get(f):
report(path, f"{lbl} ({ex.get('name','?')}) missing '{f}'", log_date)
if "exercise_id" not in ex:
report(path, f"{lbl} missing 'exercise_id' (a program key, or null for a one-off)", log_date)
elif ex["exercise_id"] is not None and ex["exercise_id"] not in lib:
report(path, f"{lbl} exercise_id '{ex['exercise_id']}' not a program/current.yaml key", log_date)
pat = ex.get("movement_pattern")
for p in (pat if isinstance(pat, list) else [pat]):
if p and p not in patterns:
report(path, f"{lbl} movement_pattern '{p}' is not tagged by any library entry", log_date)
if "sets" not in ex:
report(path, f"{lbl} ({ex.get('name','?')}) missing 'sets'", log_date)
elif stype in VALID_TYPES and not doc.get("notes") and not doc.get("exercises"):
report(path, f"{stype} session needs a narrative 'notes' block", log_date)
prs = doc.get("prs")
if prs is not None:
if not isinstance(prs, list):
report(path, "'prs' must be a list of objects", log_date)
else:
for i, pr in enumerate(prs):
if not isinstance(pr, dict):
report(path, f"prs[{i}] must be an object", log_date)
elif not PR_KEYS.issubset(pr):
report(path, f"prs[{i}] missing keys: {sorted(PR_KEYS - set(pr))}", log_date)
def check_program(program, patterns):
for kind in ("exercises", "cardio"):
for eid, ex in (program.get(kind) or {}).items():
ls = (ex.get("current") or {}).get("last_session")
if ls is not None and to_date(ls) is None:
errors.append(f"program/current.yaml: {kind}.{eid} current.last_session invalid date '{ls}'")
for k, cov in (program.get("pattern_coverage") or {}).items():
if k not in patterns:
errors.append(f"program/current.yaml: pattern_coverage key '{k}' has no library entry that trains it")
for c in (cov or {}).get("covered_by", []):
if c not in patterns:
errors.append(f"program/current.yaml: pattern_coverage.{k} covered_by '{c}' has no library entry")
def check_prs_file(lib):
data = yaml.safe_load((REPO / "logs" / "prs.yaml").read_text()) or {}
for key in (data.get("records") or {}):
if key not in lib and not str(key).startswith("legacy_"):
errors.append(f"logs/prs.yaml: record key '{key}' not a program id or legacy_* key")
def main():
program = load_program()
patterns, lib = known_patterns(program), library_ids(program)
logs = sorted((REPO / "logs").glob("*.yaml"))
skip = {"TEMPLATE.yaml", "prs.yaml"}
for p in logs:
if p.name not in skip:
check_log(p, patterns, lib)
check_program(program, patterns)
check_prs_file(lib)
for w in warnings:
print(f"WARN {w}")
for e in errors:
print(f"ERROR {e}")
print(f"\n{len(logs) - len(skip)} logs checked: {len(errors)} error(s), {len(warnings)} warning(s)")
sys.exit(1 if errors else 0)
if __name__ == "__main__":
main()
The model will silently corrupt its own conventions. Across a long-running repo, an LLM drifts: the same field becomesday
thenday_of_week
; durations becomeduration_min
/duration_estimate
; 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 withexternalstructure: the documented schema (TEMPLATE.yaml), the validator, and CI on every push. TheLEGACY_FIELDS
map in validate.py exists specifically to catch this drift. -
Write each fact exactly once. Two homes: full narrative β the session log; 2-sentence state + next trigger βcurrent.trend
. Recency isn't a third home β it'sderivedfromcurrent.last_session
, 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 whyrecent.yaml
was removed: it re-stored last-trained dates that already live incurrent.yaml
.) -
Never hand-compute staleness. Runstaleness.py
; 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. -
Stable Theexercise_id
vs free-textvariant
.exercise_id
(a library key) is thematching keyβ PR and history tracking key off it, so it must be exact. Thevariant
is 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. -
Units are the disambiguator; always carry them.2x30
is ambiguous β two 30 lb dumbbells, or 2 sets of 30? Rule:weights always carry a unit(2Γ30 lb
= two 30s),setsΓreps never do(3Γ8
= 3 sets of 8). So3Γ8 @ 2Γ30 lb
is unambiguous. Put this in CLAUDE.md. -
Timezone the log filenames. The system clock may be UTC; the user isn't. Name log files and setdate:
in the user's timezone, and look upday_of_week
withTZ="<their/tz>" date -d 'YYYY-MM-DD' '+%A'
rather than guessing (the validator checks that the day matches the date). -
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. -
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. -
Effective staleness / coverage stops the coach nagging. If a recurring activity trains a goal (a weekly class coverscore
; a sport banks interval cardio), record it inpattern_coverage
so 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). -
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. -
Grandfather your schema migrations. When you change the schema mid-life, don't retro- break every old log. SetGRANDFATHER_DATE
so pre-migration logswarnand post-migration logserror. Migrate the back-catalog deliberately in its own commit, not all at once under pressure.
Before you hand the repo over, verify:
- Interview summary was played back and confirmed by the user.
- The exercise library was built from the interview, not kept as the doc's example list,
and every
movement_pattern
tag reflects a pattern the user will actually train. - Every
exercise_id
you'll log against exists as a key inprogram/current.yaml
. - Every
pattern_coverage
key andcovered_by
entry is a pattern some library entry tags. -
VALID_TYPES
invalidate.py
matches the session types from the interview (Β§1 C). - Only the scheduling block for their chosen structure level remains in the program. #
pip install pyyaml && python3 scripts/staleness.py
runs and prints a sane briefing (everything "never trained" on day one is expected). -
python3 scripts/validate.py
exits 0 (no logs yet β it's confirmingcurrent.yaml
andprs.yaml
are internally consistent). - CLAUDE.md covers everything in Β§2.6. #
git init
, 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:
open an agent in the repo and say "I'm training at<location>
today, I've got<time>
, feeling<energy>
β what should I do?"
That'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.