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.
api.telegram.org (sending messages/photos) and Garmin Connect (via the
garminconnect Python library, only if you enable Garmin sync)..credentials at 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.training/send-plan.sh,
training/text-to-image.sh or training/text-to-image-canvas.mjs yourself.
Each run prints the file names and "Sending N image(s)..." before the upload
to the chat_id you configured. Nothing is sent on a schedule or in the
background, and Garmin sync only downloads — it never uploads.bash, node (≥ 18) and, for the optional Garmin sync,
python3 with the garminconnect package. No elevated privileges; every
file the skill writes stays inside its own directory.bash
# 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).
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.
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.
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.
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.
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.
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.)
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.
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.
bash
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).
Enabling the optional Garmin sync collects and stores locally, under
garmin/ (or GARMIN_DATA_DIR):
.garth/ stores Garmin session tokens (avoids re-login)Handling model:
rm -rf garmin/activities garmin/summary.json garmin/.garthgarmin-sync.py enforces owner-only access on every run —
garmin/, garmin/activities/ and garmin/.garth/ are set to 0700, and
every snapshot, summary.json and token file to 0600 (existing files are
tightened too). No manual chmod neededgarmin/.credentials — every coaching
feature except wearable sync works without itbash
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.