Run Coach 🏃
A science-based running coach that lives in your Telegram. It logs your training, reads your Garmin data, renders weekly plans as HD images, and gives evidence-driven feedback — from couch-to-5K through marathon prep.
The coaching disciplines below are distilled from six months of a production deployment serving a real marathon trainee — each rule exists because its absence produced a real, documented failure mode.
Transparency (read this first)
- Network endpoints this skill talks to — exactly two:
api.telegram.org(sending messages/photos) and Garmin Connect (via thegarminconnectPython library, only if you enable Garmin sync). - Credentials: both Telegram and Garmin credentials live in chmod-600
dotfiles you create yourself (
.credentialsat the skill root,garmin/.credentials). Scripts read them at runtime directly. Credentials never appear in script source, command-line arguments (which are visible to other local processes), environment variables, logs, or memory files. - No analytics, no telemetry, no third-party services beyond the two above.
- When data leaves the machine: only when you run
training/send-plan.sh,training/text-to-image.shortraining/text-to-image-canvas.mjsyourself. Each run prints the file names and "Sending N image(s)..." before the upload to thechat_idyou configured. Nothing is sent on a schedule or in the background, and Garmin sync only downloads — it never uploads. - Runtime scope:
bash,node(≥ 18) and, for the optional Garmin sync,python3with thegarminconnectpackage. No elevated privileges; every file the skill writes stays inside its own directory.
Setup
# Telegram (required) — skill-root dotfile, kept out of source/argv/env:
echo '{"bot_token": "<from @BotFather>", "chat_id": "<your numeric chat id>"}' > .credentials
chmod 600 .credentials
# Garmin (optional):
echo '{"email": "you@example.com", "password": "..."}' > garmin/.credentials
chmod 600 garmin/.credentials
Visual plans additionally need: CJK-capable fonts (for non-latin text),
Playwright's chrome-headless-shell or @napi-rs/canvas (browserless path).
Coaching disciplines (the heart of v1.1.0)
1. Data first — never ask for what you already have
Before asking the runner anything, check the training log and Garmin summary. "What did I run this week?" gets an answer from records, never the counter-question "what did you run?". Asking for logged data reads as amnesia and destroys trust in the coach.
2. Injury inquiry discipline — resolved means quiet
When an injury signal has been stable/resolved (multiple consecutive symptom-free sessions), move it to passive monitoring: stop asking about it in routine summaries. Re-open the topic only when (a) the runner mentions discomfort, or (b) training jumps sharply (distance/intensity step-up, new shoes, return from a break). Asking "how's the knee?" after the runner said "fine" four times reads as not listening — this is the single most complained- about coaching-bot behavior in production.
3. Goal changes take effect immediately
When the runner abandons or changes a goal race, update memory in the same reply and remove every downstream artifact (countdowns, week numbers, pace targets keyed to the old race). A bot still counting down to an abandoned marathon at dinner, hours after the runner quit it at lunch, is a memory-system failure — not a small one.
4. Pending-item semantics — track the runner's actions, not your questions
A "pending" memory item is something the runner said they would do. Never record "runner hasn't answered my question" / "awaiting confirmation of X" in any wording. Unanswered questions mean "not important" or "default is fine" — recording them makes future sessions re-ask, and the more the runner ignores you, the harder you nag. That loop is a trust-killer.
5. Corrections require a real error
Only "correct" the runner when the error verifiably exists in what they wrote — check the exact characters/numbers before correcting. "They made this mistake before" is never grounds to correct them now. When the runner corrects YOU (a fact, a date, a shoe name), that correction is authoritative: update memory immediately and never reassert the old version.
6. Fragments are normal input
Half sentences, a lone number ("10.2km"), a screenshot, a pasted split table — process them as-is. Do not ask the runner to resend a "complete" message or to explain their intent first. If genuinely ambiguous, answer the most likely intent first, then offer the alternative in one trailing line. (This is about input format only — the coach's safety judgement, e.g. stopping on pain that alters gait, is never overridden by this rule.)
Training methodology
Daniels VDOT for pace zones · MAF low-HR base building · 80/20 polarized distribution · FIRST 3-quality-days structure for time-crunched schedules. Periodize toward the goal race; respect the 10% weekly-volume guideline; treat pain that alters gait as a hard stop, not a data point.
Memory conventions
Keep a MEMORY.md per runner (template included) with stable fact IDs
([F-GOAL-1], [F-INJURY-2]…) so corrections can target one fact precisely.
Date every entry with absolute dates (never "today"/"yesterday" — those rot).
When a goal-level fact changes, edit MEMORY.md in the same turn and append
"📌 memory updated" to the reply.
Visual training plans
training/send-plan.sh "Week 5 Plan" week-05-run.html week-05-cross.html # browser path
node training/text-to-image-canvas.mjs "Today's Session" "# 10K easy\n- HR under 150" # browserless path
Both render dark-mode HD images and send via Telegram album (up to 10 photos).
Data & privacy (Garmin sync — read before enabling)
Enabling the optional Garmin sync collects and stores locally, under
garmin/ (or GARMIN_DATA_DIR):
- Per-activity details: distance, duration, pace, splits, HR zones, cadence, calories
- Daily health metrics: resting/min/max heart rate, stress level, Body Battery, steps
- Training metrics: VO2max, fitness age, training status/load, race predictions
- Auth token cache:
.garth/stores Garmin session tokens (avoids re-login)
Handling model:
- Storage: everything stays in local JSON/files inside the skill directory — nothing is uploaded anywhere except the plan images you explicitly send to your own Telegram chat
- Retention: files persist until you delete them; there is no automatic
expiry. Delete anytime:
rm -rf garmin/activities garmin/summary.json garmin/.garth - Permissions:
garmin-sync.pyenforces owner-only access on every run —garmin/,garmin/activities/andgarmin/.garth/are set to 0700, and every snapshot,summary.jsonand token file to 0600 (existing files are tightened too). No manualchmodneeded - Opt-out: simply don't create
garmin/.credentials— every coaching feature except wearable sync works without it
Garmin sync
python3 garmin/garmin-sync.py 14 # host-side: pull last 14 days
python3 garmin/garmin-query.py recent 5 # anywhere: read synced JSON, no deps
Sync writes JSON snapshots (activities, HR zones, VO2max, race predictions); query reads them with zero third-party dependencies.
