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
110 lines
4.9 KiB
Markdown
110 lines
4.9 KiB
Markdown
---
|
||
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`). |