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
This commit is contained in:
IT Guy
2026-09-20 07:47:05 +00:00
parent 42f37a89f8
commit 109162889c
6 changed files with 993 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
---
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`).