Files
IT Guy 109162889c feat(skills): add zwift-workout skill (.zwo builder, validator, preview, publishing)
Bike-only Zwift .zwo generator with human-readable preview + confirmation loop.

- scripts/zwo_builder.py: spec<->.zwo build/validate/parse/render/TSS estimate
- scripts/zwo.py: CLI (build, from-template, validate, parse, render, templates)
- references/zwo-format.md: condensed offline format reference
- assets/example-vo2max.zwo + tests/test_zwo.py (9 tests)
- Publishes via hosting skill; files result into Knowledge Wiki

Format ground truth: h4l/zwift-workout-file-reference
2026-09-20 07:47:05 +00:00

110 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: zwift-workout
description: Create, preview, edit, validate and publish Zwift .zwo workout files for cycling (bike-only). Use when the user asks to build a Zwift workout, generate a .zwo file, convert a training idea into a Zwift session, preview/adjust an existing workout, or publish a workout for import into Zwift.
---
# 🚴 Zwift Workout (.zwo) Builder
Builds valid Zwift `.zwo` files from a small JSON spec, renders a human-readable
preview for confirmation, and publishes the finished file. **Bike-only.**
## When to use
- "Bau mir ein Zwift-Workout …" / "Create a .zwo for 5×3min VO2max"
- "Zeig mir das Workout" / "änder die Intervalle auf 4×5min"
- "Lade das Workout in Zwift" (publish to hosting for import)
## Core workflow (always follow this order)
1. **Draft the spec.** Either pick a deterministic template
(`zwo.py templates` → `endurance | sweetspot | vo2max | sprint`) or write a
JSON spec from the user's request. Keep power as FTP fractions
(`0.88` = 88 %, `1.15` = 115 %); durations in **seconds**.
2. **Render the preview and ask for confirmation.** Run
`python3 {baseDir}/scripts/zwo.py render <spec.json>` (or `build … --preview`)
and show the table to the user. **Do not publish before the user confirms.**
3. **Apply changes.** Edit the JSON spec and re-render; repeat until confirmed.
4. **Build + publish on confirmation.** Run
`python3 {baseDir}/scripts/zwo.py build <spec.json> -o <name>.zwo --publish`.
This validates, writes the file, and uploads it via the `hosting` skill
(`https://hosting.haumdaucher.de/docs/…`).
5. **Deliver.** Post the hosting link (and the `.zwo` as `MEDIA:<path>` if the
user wants the file directly). Optionally offer a QR code (hosting skill rule).
6. **File the result into the wiki** under `syntheses/` or `concepts/` per the
Knowledge Wiki protocol (see below).
## JSON spec format
```json
{
"name": "VO2 Max 5x3",
"author": "IT Guy",
"description": "5 × 3 min @ 115 % FTP, 3 min Pause.",
"sportType": "bike",
"tags": ["vo2max", "structured"],
"steps": [
{"type": "Warmup", "duration": 600, "powerLow": 0.50, "powerHigh": 0.75},
{"type": "IntervalsT", "repeat": 5, "onDuration": 180, "offDuration": 180,
"onPower": 1.15, "offPower": 0.55},
{"type": "Cooldown", "duration": 300, "powerLow": 0.75, "powerHigh": 0.40}
]
}
```
### Supported step types (bike)
| `type` | Required fields | Optional |
|---|---|---|
| `Warmup` | `duration`, `powerLow`, `powerHigh` | `cadence` |
| `Cooldown` | `duration`, `powerLow`, `powerHigh` | `cadence` |
| `SteadyState` | `duration`, `power` | `cadence` |
| `Ramp` | `duration`, `powerLow`, `powerHigh` | — |
| `IntervalsT` | `repeat`, `onDuration`, `offDuration`, `onPower`, `offPower` | `cadence`, `cadenceResting` |
| `FreeRide` | `duration` | `flatRoad` |
| `MaxEffort` | `duration` | — |
| `SolidState` | `duration`, `power` | — |
| `RestDay` | — | — |
Power values are **fractions of FTP**; duration is in **seconds**. See
`references/zwo-format.md` for the full tag/attribute reference.
## CLI
```bash
python3 {baseDir}/scripts/zwo.py templates # list templates
python3 {baseDir}/scripts/zwo.py from-template vo2max --title "VO2 Max 5x3" --publish
python3 {baseDir}/scripts/zwo.py render my.json # preview only
python3 {baseDir}/scripts/zwo.py build my.json -o out.zwo --preview --publish
python3 {baseDir}/scripts/zwo.py validate existing.zwo # check a file
python3 {baseDir}/scripts/zwo.py parse existing.zwo --json # .zwo -> spec (editable)
```
## Guardrails
- **Power range:** `0.20 – 1.50` FTP. Outside → hard error (catches unit mistakes).
- **Durations:** integer seconds, `> 0`; `< 5 s` and `> 4 h` produce warnings.
- **Structure:** exactly one `<workout>`, root `<workout_file>`, `name` required,
`sportType` must be `bike`.
- **Round-trip check:** output XML is re-parsed with `xml.etree` before writing;
text fields are escaped, so umlauts/quotes never corrupt the file.
- **Never publish without explicit user confirmation** of the rendered preview.
## Reference & fixtures
- `references/zwo-format.md` — condensed tag/attribute reference (offline copy).
- `assets/example-vo2max.zwo` — known-good fixture; use for regression tests.
## Knowledge Wiki filing
After a workout is confirmed and published, file it per the Karpathy LLM Wiki
protocol: create/update a `concepts/` or `syntheses/` page recording the workout
name, structure, power targets, hosting URL, and rationale, then append a
`log.md` entry (`ingest`/`synthesis`). Cross-link the Fitness agent page.
## Inter-agent synergy
`agents/fitness.md` owns training intent (Tacx Flux 2, FTP 275 W). This skill is
the execution layer for structured cycling sessions. For evidence-based
protocols (e.g. SIT 30 s / 4 min), consult the Knowledge Wiki first via
`wiki_search`. A future refactor can share one spec module with `garmin-workout`
(one source spec → Garmin workout + `.zwo`).