@wattsonclub/wattson-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +118 -0
- package/dist/bin.js +366 -192
- package/package.json +8 -3
- package/skill/SKILL.md +9 -7
- package/skill/references/commands.md +197 -292
- package/skill/references/workout-building-blocks.md +266 -481
- package/skill/references/workout-text-format.md +71 -409
|
@@ -1,507 +1,292 @@
|
|
|
1
|
-
# Workout building blocks
|
|
1
|
+
# Workout building blocks: exact authoring contract
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
It is intentionally agent-oriented.
|
|
6
|
-
|
|
7
|
-
Goal:
|
|
8
|
-
- make the workout structure legible without reading raw Zod code
|
|
9
|
-
- expose the atomic building blocks that agents can combine
|
|
10
|
-
- make validation/revision loops more reliable
|
|
11
|
-
- establish a stable conceptual vocabulary for future CLI/MCP/chat interfaces
|
|
12
|
-
|
|
13
|
-
Canonical source of truth remains the domain schema.
|
|
14
|
-
This doc is a human/agent guide, not a replacement for the schema.
|
|
15
|
-
|
|
16
|
-
Source of truth:
|
|
17
|
-
- `packages/domain/src/index.ts`
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## 1. Mental model
|
|
22
|
-
|
|
23
|
-
A workout is:
|
|
24
|
-
- metadata + rationale + constraints
|
|
25
|
-
- a sequence of **blocks**
|
|
26
|
-
- where blocks are either:
|
|
27
|
-
- simple timed blocks
|
|
28
|
-
- repeated interval sets
|
|
29
|
-
- notes/support markers
|
|
30
|
-
|
|
31
|
-
The useful abstraction for agents is:
|
|
32
|
-
|
|
33
|
-
1. choose an **objective**
|
|
34
|
-
2. choose a **macro shape**
|
|
35
|
-
3. fill that shape with typed blocks
|
|
36
|
-
4. choose targets/execution modes/cadence/HR constraints
|
|
37
|
-
5. validate
|
|
38
|
-
6. revise until clean
|
|
39
|
-
|
|
40
|
-
In other words:
|
|
41
|
-
- agents should compose workouts from typed pieces
|
|
42
|
-
- agents should not treat the workout as an opaque paragraph
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 2. Top-level workout fields that matter most
|
|
47
|
-
|
|
48
|
-
These fields drive authoring behavior most strongly:
|
|
49
|
-
|
|
50
|
-
- `title`
|
|
51
|
-
- `objective`
|
|
52
|
-
- `athleteContext.ftpWatts`
|
|
53
|
-
- `blocks[]`
|
|
54
|
-
- `constraints`
|
|
55
|
-
- `rationale`
|
|
56
|
-
- optional `computed`
|
|
57
|
-
- optional `validation`
|
|
58
|
-
- optional `scheduledDate` (`YYYY-MM-DD`): the day the rider plans to ride it. Set with `schedule_saved_workout`; it orders the library and never starts a ride.
|
|
59
|
-
|
|
60
|
-
Important practical point:
|
|
61
|
-
- the **blocks** define the actual workout shape
|
|
62
|
-
- the **constraints** define what is allowed/safe/preferred
|
|
63
|
-
- the **validation/computed** fields define whether the workout is coherent/executable
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## 3. Atomic building blocks
|
|
68
|
-
|
|
69
|
-
## 3.1 Simple timed blocks
|
|
70
|
-
|
|
71
|
-
These are the main single-step building blocks.
|
|
72
|
-
|
|
73
|
-
Supported simple block types:
|
|
74
|
-
- `warmup`
|
|
75
|
-
- `steady`
|
|
76
|
-
- `recovery`
|
|
77
|
-
- `cooldown`
|
|
78
|
-
- `ramp`
|
|
79
|
-
- `free_ride`
|
|
80
|
-
- `cadence_drill`
|
|
81
|
-
- `rest`
|
|
82
|
-
|
|
83
|
-
Shape:
|
|
84
|
-
- `id`
|
|
85
|
-
- `type`
|
|
86
|
-
- `durationSec`
|
|
87
|
-
- optional `target`
|
|
88
|
-
- optional `title`
|
|
89
|
-
- optional `instructions[]`
|
|
90
|
-
- optional `cadence`
|
|
91
|
-
- optional `hr`
|
|
92
|
-
|
|
93
|
-
### When to use them
|
|
94
|
-
|
|
95
|
-
#### `warmup`
|
|
96
|
-
Use for:
|
|
97
|
-
- gradually preparing the rider
|
|
98
|
-
- low-to-moderate progressive openers
|
|
99
|
-
- cadence ramping without heavy load
|
|
100
|
-
|
|
101
|
-
#### `steady`
|
|
102
|
-
Use for:
|
|
103
|
-
- endurance
|
|
104
|
-
- tempo
|
|
105
|
-
- threshold holds
|
|
106
|
-
- any stable target period
|
|
107
|
-
|
|
108
|
-
#### `recovery`
|
|
109
|
-
Use for:
|
|
110
|
-
- low-load valleys between work bouts
|
|
111
|
-
- aerobic reset periods
|
|
112
|
-
|
|
113
|
-
#### `cooldown`
|
|
114
|
-
Use for:
|
|
115
|
-
- winding the session down
|
|
116
|
-
- reducing target and cadence pressure at the end
|
|
117
|
-
|
|
118
|
-
#### `ramp`
|
|
119
|
-
Use for:
|
|
120
|
-
- explicit increasing/decreasing load phases
|
|
121
|
-
- progressive openers or build segments
|
|
122
|
-
|
|
123
|
-
#### `free_ride`
|
|
124
|
-
Use for:
|
|
125
|
-
- rider-controlled sections
|
|
126
|
-
- open movement / transitions
|
|
127
|
-
- less prescriptive sections where target control should be loose
|
|
128
|
-
|
|
129
|
-
#### `cadence_drill`
|
|
130
|
-
Use for:
|
|
131
|
-
- skill emphasis
|
|
132
|
-
- cadence-specific work where the pedaling cue matters as much as the power target
|
|
133
|
-
|
|
134
|
-
#### `rest`
|
|
135
|
-
Use for:
|
|
136
|
-
- explicit rests distinct from active recovery
|
|
137
|
-
- true low/no-load gaps
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## 3.2 Interval set block
|
|
142
|
-
|
|
143
|
-
Composite block type:
|
|
144
|
-
- `interval_set`
|
|
145
|
-
|
|
146
|
-
Shape:
|
|
147
|
-
- `id`
|
|
148
|
-
- `type: "interval_set"`
|
|
149
|
-
- `repeat`
|
|
150
|
-
- `steps[]`
|
|
151
|
-
- optional `title`
|
|
152
|
-
- optional `instructions[]`
|
|
153
|
-
- optional `cadence`
|
|
154
|
-
- optional `hr`
|
|
155
|
-
|
|
156
|
-
This is the main repeating composition primitive.
|
|
157
|
-
|
|
158
|
-
Think of it as:
|
|
159
|
-
- a small local workout pattern
|
|
160
|
-
- repeated `n` times
|
|
161
|
-
|
|
162
|
-
That makes it the core tool for:
|
|
163
|
-
- VO2 reps
|
|
164
|
-
- threshold reps
|
|
165
|
-
- over/unders (using step structure)
|
|
166
|
-
- cadence skill repeats
|
|
167
|
-
- repeated recovery/work structures
|
|
168
|
-
|
|
169
|
-
### Allowed interval-set step types
|
|
170
|
-
Inside an `interval_set`, steps can be:
|
|
171
|
-
- `work`
|
|
172
|
-
- `recovery`
|
|
173
|
-
- `steady`
|
|
174
|
-
- `ramp`
|
|
175
|
-
|
|
176
|
-
This is important:
|
|
177
|
-
- the outer block says “repeat this structure”
|
|
178
|
-
- the inner steps say what the repeated structure actually is
|
|
179
|
-
|
|
180
|
-
### Good uses
|
|
181
|
-
|
|
182
|
-
#### Classic 3x VO2
|
|
183
|
-
- interval_set repeat 3
|
|
184
|
-
- work 2m @ 115% FTP
|
|
185
|
-
- recovery 2m @ 50% FTP
|
|
186
|
-
|
|
187
|
-
#### Threshold ladder
|
|
188
|
-
- interval_set repeat 2
|
|
189
|
-
- steady 6m @ 100% FTP
|
|
190
|
-
- recovery 2m @ 55% FTP
|
|
191
|
-
|
|
192
|
-
#### Cadence focus set
|
|
193
|
-
- interval_set repeat 4
|
|
194
|
-
- steady 1m @ 75% FTP cadence 105+
|
|
195
|
-
- recovery 1m @ 50% FTP
|
|
196
|
-
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
## 3.3 Climb block
|
|
200
|
-
|
|
201
|
-
Composite terrain block:
|
|
202
|
-
- `climb`
|
|
203
|
-
|
|
204
|
-
Shape:
|
|
205
|
-
- `id`
|
|
206
|
-
- `type: "climb"`
|
|
207
|
-
- `course` (a `CourseProfile`: name, source, totals, and `segments[]` of `{ distanceM, gradePct }`)
|
|
208
|
-
- optional `pacingPctFtp` (default `0.85`)
|
|
209
|
-
- optional `title`, `instructions[]`, `cadence`, `hr`
|
|
210
|
-
|
|
211
|
-
A climb is a simulated ascent: the trainer runs in gradient simulation mode and follows the course's grade
|
|
212
|
-
at the rider's virtual position, which the app computes from live watts, rider and bike mass, and the
|
|
213
|
-
profile. The block ends when the rider reaches the end of the course, not on a timer. Its compiled
|
|
214
|
-
`durationSec` is a physics estimate at `pacingPctFtp × FTP` and drives previews, TSS and zone time only.
|
|
215
|
-
|
|
216
|
-
Rules:
|
|
217
|
-
- `constraints.allowedExecutionModes` must include `"grade"`.
|
|
218
|
-
- Get `course` from the planner's climb catalog (`lookup_climb`), a Strava segment import
|
|
219
|
-
(`import_strava_segment`), one of the rider's own Strava rides (`import_strava_activity_course`, source
|
|
220
|
-
kind `strava_activity`, optionally cut to a segment effort or distance window), a Strava route
|
|
221
|
-
(`import_strava_route`, source kind `strava_route`), a GPX file or public GPX link (`import_gpx_course`,
|
|
222
|
-
source kind `gpx`), a myCols or climbfinder page (`import_climb_page`, source kind `climb_page`), the
|
|
223
|
-
road between two places (`build_road_course`, source kind `road`, OpenStreetMap
|
|
224
|
-
geometry plus raster elevation) or generated terrain
|
|
225
|
-
(`generate_terrain_course`, source kind `synthetic`: a distance, average grade and shape, deterministic
|
|
226
|
-
for the same inputs). Saved workouts keep a snapshot of the course, so disconnecting Strava does not
|
|
227
|
-
remove the climb.
|
|
228
|
-
- `athleteContext.weightKg` improves the estimate (75 kg when absent). Bike mass is fixed internally at 7.5 kg; legacy profile/workout bike weights are ignored. W/kg uses rider weight only.
|
|
229
|
-
- Trainers without simulation mode ride the climb in ERG at the pacing watts.
|
|
230
|
-
|
|
231
|
-
Validation warns when a course is under 300 m, averages under 1 %, or has grades outside −20..25 % (they
|
|
232
|
-
are clamped for the trainer).
|
|
233
|
-
|
|
234
|
-
Every ride also gets a virtual distance (`actualMetrics.distanceM`, `ascentM`): climbs from the course
|
|
235
|
-
odometer, ERG steps as a flat road at the recorded power, written into the FIT file so Strava counts the
|
|
236
|
-
mileage. After the ride, `summarize_session` reports each climb the rider started (`summary.climbs`): distance and
|
|
237
|
-
ascent actually ridden from the app's virtual odometer, time, average power, VAM and whether the summit was
|
|
238
|
-
reached. The app's ride history shows the same figures with the profile.
|
|
239
|
-
|
|
240
|
-
## 3.4 Note block
|
|
241
|
-
|
|
242
|
-
Support-only block type:
|
|
243
|
-
- `note`
|
|
244
|
-
|
|
245
|
-
Shape:
|
|
246
|
-
- `id`
|
|
247
|
-
- `type: "note"`
|
|
248
|
-
- `text`
|
|
249
|
-
|
|
250
|
-
Use for:
|
|
251
|
-
- non-executable guidance
|
|
252
|
-
- rider coaching context
|
|
253
|
-
- explicit reminders that should survive revision/rendering
|
|
254
|
-
|
|
255
|
-
Agents should use `note` sparingly.
|
|
256
|
-
If the content changes the actual executable session shape, it should probably be a real workout block, not just a note.
|
|
257
|
-
|
|
258
|
-
---
|
|
259
|
-
|
|
260
|
-
## 4. Target grammar
|
|
261
|
-
|
|
262
|
-
Targets are also typed.
|
|
263
|
-
This matters because agents should choose targets intentionally rather than describing them vaguely.
|
|
264
|
-
|
|
265
|
-
Supported target modes:
|
|
266
|
-
- `open`
|
|
267
|
-
- `watts`
|
|
268
|
-
- `pct_ftp`
|
|
269
|
-
- `zone`
|
|
270
|
-
- `ramp_watts`
|
|
271
|
-
- `ramp_pct_ftp`
|
|
272
|
-
- `resistance_level`
|
|
273
|
-
- `grade_percent`
|
|
274
|
-
|
|
275
|
-
## 4.1 Practical meaning
|
|
276
|
-
|
|
277
|
-
### `open`
|
|
278
|
-
No strict load target.
|
|
279
|
-
Good for:
|
|
280
|
-
- free ride
|
|
281
|
-
- easy reset
|
|
282
|
-
- technique-focused movement
|
|
283
|
-
|
|
284
|
-
### `watts`
|
|
285
|
-
Absolute target.
|
|
286
|
-
Good when:
|
|
287
|
-
- athlete FTP/context is already resolved
|
|
288
|
-
- exact prescription matters
|
|
289
|
-
|
|
290
|
-
### `pct_ftp`
|
|
291
|
-
Relative target.
|
|
292
|
-
Good default for authoring because:
|
|
293
|
-
- portable across riders
|
|
294
|
-
- easier for agents to reason about conceptually
|
|
295
|
-
|
|
296
|
-
Important:
|
|
297
|
-
- `%FTP` values are fractional in structured payloads
|
|
298
|
-
- use `0.55` for `55% FTP`
|
|
299
|
-
- use `1.15` for `115% FTP`
|
|
300
|
-
- never pass whole percentages like `55` or `115`
|
|
301
|
-
|
|
302
|
-
### `zone`
|
|
303
|
-
Zone-based target.
|
|
304
|
-
Good for:
|
|
305
|
-
- simple endurance/recovery prescriptions
|
|
306
|
-
- more human-facing authoring when exact percentage is unnecessary
|
|
307
|
-
|
|
308
|
-
### `ramp_watts` / `ramp_pct_ftp`
|
|
309
|
-
Progressive target from start -> end.
|
|
310
|
-
Good for:
|
|
311
|
-
- warmups
|
|
312
|
-
- build phases
|
|
313
|
-
- progressive efforts
|
|
314
|
-
|
|
315
|
-
### `resistance_level`
|
|
316
|
-
Trainer resistance mode target.
|
|
317
|
-
Use when explicitly designing for resistance execution.
|
|
318
|
-
|
|
319
|
-
### `grade_percent`
|
|
320
|
-
Grade-mode target.
|
|
321
|
-
Use when explicitly designing for grade/simulation execution.
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## 5. Additional per-block constraints/cues
|
|
326
|
-
|
|
327
|
-
Blocks and interval steps can also carry:
|
|
328
|
-
|
|
329
|
-
### cadence
|
|
330
|
-
Shape:
|
|
331
|
-
- `min`
|
|
332
|
-
- `max`
|
|
333
|
-
- `cue`
|
|
334
|
-
|
|
335
|
-
Use for:
|
|
336
|
-
- skill emphasis
|
|
337
|
-
- spin-up drills
|
|
338
|
-
- low-cadence strength cues
|
|
339
|
-
|
|
340
|
-
### hr
|
|
341
|
-
Shape:
|
|
342
|
-
- `capBpm`
|
|
343
|
-
- `softCapBpm`
|
|
344
|
-
|
|
345
|
-
Use for:
|
|
346
|
-
- safety bounds
|
|
347
|
-
- recovery gating
|
|
348
|
-
- keeping endurance/recovery blocks honest
|
|
349
|
-
|
|
350
|
-
### instructions[]
|
|
351
|
-
Use for:
|
|
352
|
-
- rider-facing execution guidance
|
|
353
|
-
- short coaching prompts
|
|
354
|
-
- reminders that belong to the block itself
|
|
355
|
-
|
|
356
|
-
Good examples:
|
|
357
|
-
- `Relax shoulders`
|
|
358
|
-
- `Stay seated`
|
|
359
|
-
- `Smooth pedal stroke`
|
|
360
|
-
- `Do not sprint the start`
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
## 6. Workout-level constraints
|
|
365
|
-
|
|
366
|
-
Workout-level constraints currently include:
|
|
367
|
-
- `maxDurationSec`
|
|
368
|
-
- `maxPctFtp`
|
|
369
|
-
- `maxContinuousWorkSec`
|
|
370
|
-
- `maxHrBpm`
|
|
371
|
-
- `allowedExecutionModes`
|
|
372
|
-
- `disallowStanding`
|
|
373
|
-
- `avoidSprintStarts`
|
|
374
|
-
- `indoorOnly`
|
|
375
|
-
|
|
376
|
-
These are important because they tell the agent what *not* to build.
|
|
377
|
-
|
|
378
|
-
Important:
|
|
379
|
-
- `maxPctFtp` is also fractional
|
|
380
|
-
- use `1.2` for `120% FTP`, not `120`
|
|
381
|
-
|
|
382
|
-
Practical rule:
|
|
383
|
-
- if the user request conflicts with workout constraints, the agent should prefer a legal workout + explanation over inventing an invalid one and hoping validation fixes it later
|
|
384
|
-
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
## 7. Composition patterns agents should use
|
|
388
|
-
|
|
389
|
-
These are not hardcoded templates.
|
|
390
|
-
They are strong composition patterns.
|
|
391
|
-
|
|
392
|
-
## 7.1 Endurance ride with gentle structure
|
|
393
|
-
- warmup
|
|
394
|
-
- steady endurance block
|
|
395
|
-
- optional cadence drill inserts
|
|
396
|
-
- cooldown
|
|
397
|
-
|
|
398
|
-
## 7.2 VO2 session
|
|
399
|
-
- warmup
|
|
400
|
-
- interval_set(work/recovery x n)
|
|
401
|
-
- cooldown
|
|
402
|
-
|
|
403
|
-
## 7.3 Threshold / sweet spot session
|
|
404
|
-
- warmup
|
|
405
|
-
- longer steady work block(s)
|
|
406
|
-
- recovery between blocks if needed
|
|
407
|
-
- cooldown
|
|
408
|
-
|
|
409
|
-
## 7.4 Skills session
|
|
410
|
-
- warmup
|
|
411
|
-
- cadence_drill or free_ride sections with explicit cadence cues
|
|
412
|
-
- light steady aerobic work
|
|
413
|
-
- cooldown
|
|
3
|
+
Structured JSON is canonical. The renderer is implemented; no workout-text parser exists. See [workout-text-format.md](workout-text-format.md). Audited against `packages/domain/src/index.ts`, `packages/domain/src/course.ts`, `packages/contracts/src/tools.ts`, and `services/planner-api/src/service.ts`.
|
|
414
4
|
|
|
415
|
-
##
|
|
416
|
-
- warmup or very gentle open start
|
|
417
|
-
- low-load steady / open blocks
|
|
418
|
-
- cadence comfort cues
|
|
419
|
-
- cooldown optional if already very easy
|
|
5
|
+
## Drafting versus composing
|
|
420
6
|
|
|
421
|
-
|
|
7
|
+
`draft_workout` generates a deterministic starter, not an arbitrary-block authoring language. Input is:
|
|
422
8
|
|
|
423
|
-
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"objective": "vo2max",
|
|
12
|
+
"durationMin": 20,
|
|
13
|
+
"constraints": { "maxPctFtp": 1.2, "allowedExecutionModes": ["erg"] },
|
|
14
|
+
"context": { "styleHints": ["three controlled efforts"] }
|
|
15
|
+
}
|
|
16
|
+
```
|
|
424
17
|
|
|
425
|
-
|
|
18
|
+
Both MCP surfaces register the name `draft_workout`. The internal explicit-athlete surface uses `DraftWorkoutInputSchema` and also requires `athleteId`; the authenticated surface uses `DraftMyWorkoutInputSchema`, omits `athleteId`, and resolves the signed-in athlete. `draftMyWorkout` is a client/service method, not a separate MCP tool name. `durationMin` is a positive integer (no schema maximum; resulting workouts must be at most 24 hours). Only `maxPctFtp` and `allowedExecutionModes` are accepted draft constraints. `context.readinessSnapshotId` is an optional nonempty ID; it is not consulted by this deterministic builder. `styleHints` defaults to `[]` and is copied into rationale, **not interpreted to change blocks**. Mode preference is ERG, then resistance, then grade. Allowing grade does not generate a distance-based climb.
|
|
426
19
|
|
|
427
|
-
|
|
428
|
-
2. infer intended duration
|
|
429
|
-
3. choose whether the workout is mostly:
|
|
430
|
-
- steady/simple blocks
|
|
431
|
-
- repeated intervals
|
|
432
|
-
- skills/free ride
|
|
433
|
-
4. choose target style:
|
|
434
|
-
- `pct_ftp` is usually the safest authoring default
|
|
435
|
-
- `zone` is okay for simpler low-intensity prescriptions
|
|
436
|
-
- `watts` is better when exact absolute load matters
|
|
437
|
-
5. add cadence / HR cues only when they actually help
|
|
438
|
-
6. validate before saving
|
|
20
|
+
For custom composition, take the returned `.workout`, edit its `blocks`, constraints, title and rationale, then pass `{ "workout": ... }` to validate and save. Draft, revise and validate are candidates/read-only with respect to the saved library; **save publishes**. Unknown object properties are generally stripped by Zod, not implemented: putting `blocks`, a custom title, or extra constraints in draft input does not author them.
|
|
439
21
|
|
|
440
|
-
|
|
22
|
+
## Complete workout envelope
|
|
441
23
|
|
|
442
|
-
|
|
24
|
+
Required fields (unless a default is stated):
|
|
443
25
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
26
|
+
| Field | Contract |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `id`, `athleteId` | Nonempty strings. Preserve server-returned identity when editing a candidate. |
|
|
29
|
+
| `version` | Positive integer. Fresh draft is 1; revision candidate is base version + 1. |
|
|
30
|
+
| `createdAt`, `updatedAt` | ISO datetime strings. |
|
|
31
|
+
| `createdBy`, `updatedBy` | Actor objects below. |
|
|
32
|
+
| `kind`, `sport` | Exactly `workout_definition`, `cycling_indoor`. |
|
|
33
|
+
| `title` | Nonempty string (workout schema has no maximum). |
|
|
34
|
+
| `objective` | `recovery`, `endurance`, `tempo`, `sweet_spot`, `threshold`, `vo2max`, `anaerobic`, `sprint`, `skills`, `test`, `custom`. |
|
|
35
|
+
| `status` | `draft`, `validated`, `scheduled`, `retired`. Save recalculates status; do not claim validation by changing this field. |
|
|
36
|
+
| `athleteContext` | Required `ftpWatts` > 0; optional `weightKg` > 0 and positive integer `maxHrBpm`; `constraints` string array defaults to `[]`. This context is a snapshot, not a live profile reference. |
|
|
37
|
+
| `blocks` | Nonempty array of blocks below. At least one executable block is needed. |
|
|
38
|
+
| `constraints` | Defaults to the constraint defaults below. |
|
|
39
|
+
| `rationale` | Required `summary` string (can be empty); `whyToday` string array defaults `[]`; optional `adaptationGoal` string, `confidence` in [0,1]. |
|
|
40
|
+
| `tags` | String array, default `[]`. |
|
|
452
41
|
|
|
453
|
-
|
|
42
|
+
Optional: `scheduledDate` (`YYYY-MM-DD` regex; use scheduling tools to manage the library day, never to start a ride), `sourceWorkoutId` (nonempty string), `computed`, `validation`, `sharing`, `workoutFingerprint`.
|
|
454
43
|
|
|
455
|
-
|
|
456
|
-
1. draft from blocks
|
|
457
|
-
2. validate
|
|
458
|
-
3. inspect issue `severity`
|
|
459
|
-
4. use issue `path` to identify which block or step is the problem
|
|
460
|
-
5. revise only the relevant part
|
|
461
|
-
6. validate again
|
|
44
|
+
Actors are exactly one of:
|
|
462
45
|
|
|
463
|
-
|
|
46
|
+
```json
|
|
47
|
+
{"type":"user","userId":"user-id"}
|
|
48
|
+
{"type":"agent","agentId":"agent-id","runId":"optional-run-id","promptRef":"optional-prompt-id"}
|
|
49
|
+
{"type":"system","system":"validator"}
|
|
50
|
+
```
|
|
464
51
|
|
|
465
|
-
|
|
52
|
+
System choices: `scheduler`, `runtime`, `validator`, `importer`. IDs are nonempty. Retain draft attribution rather than fabricate actors. `sharing` is server-owned lineage: `originalWorkoutId`, `originalWorkoutVersion`, `originalShareId`, `originalFingerprint`, `sourceWorkoutId`, `sourceWorkoutVersion`, `shareId`. Versions are positive integers, share IDs UUIDs, fingerprints match `^v1:[a-f0-9]{64}$`. After input schema parsing, `saveWorkout` overwrites `sharing` with the latest saved record’s lineage, or clears it for a new ID, then computes `workoutFingerprint`; do not synthesize either. This ownership rule concerns `sharing`, not the separate top-level `sourceWorkoutId`. Malformed supplied lineage can still fail input parsing before it is discarded.
|
|
466
53
|
|
|
467
|
-
|
|
54
|
+
`computed` has positive integer `durationSec`, nonnegative `intensityFactor`, `tss`, optional nonnegative `estimatedKj`, and `zoneDistributionSec` with nonnegative z1–z5 and optional z6–z7. `validation` has `status: pass|warn|fail`, `issues` default `[]`; each issue has string `code`, `message`, string-array `path` default `[]`, and `severity: warning|error`. Remove stale computed/validation after edits; validation/save recompute them.
|
|
468
55
|
|
|
469
|
-
|
|
470
|
-
- use a clear warmup / work / cooldown shape
|
|
471
|
-
- use `pct_ftp` rather than over-specific absolute watts too early
|
|
472
|
-
- keep repeated sets explicit with `interval_set`
|
|
473
|
-
- keep rider coaching in `instructions[]` rather than in long prose
|
|
474
|
-
- use workout constraints honestly
|
|
475
|
-
- treat `validate` as part of authoring, not as a final optional extra
|
|
56
|
+
## All block primitives
|
|
476
57
|
|
|
477
|
-
|
|
58
|
+
Executable blocks share `id` (nonempty), optional `title` string, `instructions` string array default `[]`, optional `cadence` and `hr`.
|
|
478
59
|
|
|
479
|
-
|
|
60
|
+
| Type | Required additional fields | Meaning |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `warmup`, `steady`, `recovery`, `cooldown`, `ramp`, `free_ride`, `cadence_drill`, `rest` | `durationSec`: positive integer; optional `target` | One timed step. Type alone does **not** set power, ramp, or trainer mode. `rest` is not automatically zero watts; `free_ride` with a power target is ERG. |
|
|
63
|
+
| `interval_set` | `repeat`: positive integer; `steps`: nonempty array | Repeat the complete step sequence. No outer duration or target. |
|
|
64
|
+
| `climb` | `course`: full course object; optional `pacingPctFtp` | Distance-ended simulation; no authored `durationSec` or `target`. |
|
|
65
|
+
| `note` | `id`, `type`, `text`: nonempty string | No duration, target, instructions, cadence, HR or title. Not executable. |
|
|
480
66
|
|
|
481
|
-
|
|
482
|
-
If it affects the ride shape, make it a block.
|
|
67
|
+
Interval steps use the shared fields plus positive integer `durationSec` and optional `target`, but **only** `work`, `recovery`, `steady`, `ramp` types. No nested sets, climbs, notes, warmups, cooldowns, cadence-drill or rest step types. `work` is not a top-level simple type. Use a `steady` interval step with cadence cues for repeated skills work. There is no schema maximum repeat/step count; keep expansions reasonable and total duration <= 86,400 seconds.
|
|
483
68
|
|
|
484
|
-
|
|
485
|
-
The more vague the workout description, the harder it is to revise and validate.
|
|
69
|
+
Interval compilation expands every repetition. Runtime IDs are `<set-id>:r1:s1` etc., not the step's authored ID. Parent and step instructions merge with duplicate strings removed. Cadence minimum/cue use child value when present, otherwise parent; cadence maximum and both HR caps use the **lower** parent/child value. Parent set title does not become each step title. Notes disappear from execution.
|
|
486
70
|
|
|
487
|
-
|
|
488
|
-
If the workout is not yet running, revise the saved workout.
|
|
489
|
-
Do not force everything through `session adapt`.
|
|
71
|
+
## Combining primitives into a session
|
|
490
72
|
|
|
491
|
-
|
|
492
|
-
Current live ops are bounded.
|
|
493
|
-
Structural edits like “add another interval right now” are not first-class runtime ops today.
|
|
73
|
+
`blocks` execute in array order; there is no parallel execution or conditional branching. Each `interval_set` expands its whole sequence for every repetition before the next top-level block begins. Recovery at the end of a set repeats too, including after the final effort.
|
|
494
74
|
|
|
495
|
-
|
|
496
|
-
|
|
75
|
+
| Recipe | Composition and timing |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| VO2 repetitions | Warmup → set of work + recovery → cooldown. 300 + 3 × (120 + 120) + 180 = 1,200 seconds. See the complete example below. |
|
|
78
|
+
| Over/unders | Warmup → set with two `work` steps at different FTP fractions → recovery → cooldown. Both efforts count as continuous work when each is >= .88 FTP; an “under” is not automatically recovery. |
|
|
79
|
+
| Ladder | Differently timed work/recovery steps in a set with `repeat: 1`, or multiple sets in `blocks`. Do not nest sets. |
|
|
80
|
+
| Two sets with a long break | First set → top-level recovery → second set. Put the long recovery outside the sets so it is not repeated. |
|
|
81
|
+
| ERG + terrain | Power warmup → climb with a returned full `course` → power cooldown. Allow both `erg` and `grade`; total duration includes a climb estimate, not a fixed finish time. |
|
|
82
|
+
| Repeated cadence skills | Set of `steady` steps with cadence and instructions, not `cadence_drill` children. Child cadence/HR inheritance follows the rules above. |
|
|
497
83
|
|
|
498
|
-
|
|
84
|
+
Choose targets separately from block labels: a `ramp` needs a ramp target to ramp power; a `recovery` label does not supply recovery watts. Include all non-open modes used in `allowedExecutionModes`. Calculate duration limits for the whole composition, not one repetition. Prefer distinct block IDs and retain the workout envelope; combine authored blocks, not saved workout envelopes or runtime plans.
|
|
499
85
|
|
|
500
|
-
##
|
|
86
|
+
## All target JSON variants
|
|
501
87
|
|
|
502
|
-
|
|
503
|
-
- a canonical text notation for workouts
|
|
504
|
-
- a structured/chat-friendly workout renderer
|
|
505
|
-
- validation feedback that references block/step locations more explicitly in human terms
|
|
88
|
+
Bounds are inclusive unless marked `>`. Numerical values need not be integers unless specified.
|
|
506
89
|
|
|
507
|
-
|
|
90
|
+
| Exact object example | Schema limits | Runtime |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| `{"mode":"open"}` | No value | Open |
|
|
93
|
+
| `{"mode":"watts","value":200}` | value > 0, no schema upper bound | ERG watts |
|
|
94
|
+
| `{"mode":"pct_ftp","value":0.88}` | 0 < value <= 3 | ERG, rounded fraction × FTP |
|
|
95
|
+
| `{"mode":"zone","value":"z2"}` | z1, z2, z3, z4, z5, z6, z7 | ERG using fixed fractions below |
|
|
96
|
+
| `{"mode":"ramp_watts","start":100,"end":180}` | Both > 0 | ERG start/end watts |
|
|
97
|
+
| `{"mode":"ramp_pct_ftp","start":0.55,"end":0.7}` | Each > 0 and <= 3 | ERG start/end, each rounded fraction × FTP |
|
|
98
|
+
| `{"mode":"resistance_level","value":35}` | 0..100 | Resistance |
|
|
99
|
+
| `{"mode":"grade_percent","value":4}` | −20..25 | Timed constant grade; not a distance-ended climb |
|
|
100
|
+
|
|
101
|
+
Missing target executes open. No implicit default intensity is assigned. Fractions are **0.55 for 55%**, never 55. Zero watts / zero FTP fraction is rejected; use open or a supported resistance target, not an invented zero-watt target. Ramps may ascend, descend or stay equal, and any simple block/interval step may carry any target variant.
|
|
102
|
+
|
|
103
|
+
Zone targets do not use the athlete's configured zone ranges: fixed fractions are z1=.50, z2=.68, z3=.83, z4=.98, z5=1.13, z6=1.35, z7=1.65. Ramps use their endpoint **average** for intensity validation and metrics, not endpoint peak. Check endpoints yourself when enforcing a strict peak ceiling.
|
|
104
|
+
|
|
105
|
+
## Cadence, HR and workout constraints
|
|
106
|
+
|
|
107
|
+
`cadence` is `{ "min": 85, "max": 100, "cue": "Smooth pedalling" }`; all fields optional, min/max positive integers. No min<=max check exists. `hr` is `{ "capBpm": 165, "softCapBpm": 155 }`; both optional positive integers. Empty cadence/HR objects are schema-valid. These are not conditional branching/recovery-gate instructions.
|
|
108
|
+
|
|
109
|
+
Compiler emits cadence **maximum** and the lower hard/soft HR cap as step safety bounds; cadence minimum/cue are not runtime target fields. Put essential rider cues in `instructions` too. Workout max HR / max FTP produce default safety bounds.
|
|
110
|
+
|
|
111
|
+
| Constraint | Range / default |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `maxDurationSec` | Optional positive integer; global 24-hour ceiling also applies. |
|
|
114
|
+
| `maxPctFtp` | Optional fraction > 0 and <= 3. |
|
|
115
|
+
| `maxContinuousWorkSec` | Optional positive integer. |
|
|
116
|
+
| `maxHrBpm` | Optional positive integer. Athlete context max HR alone does not set this constraint. |
|
|
117
|
+
| `allowedExecutionModes` | Array of `erg`, `resistance`, `grade`; default `["erg"]`. Empty allowed by schema. Open execution is always permitted; `open` is not an allowed-array enum value. |
|
|
118
|
+
| `disallowStanding` | Boolean default false. |
|
|
119
|
+
| `avoidSprintStarts` | Boolean default false. |
|
|
120
|
+
| `indoorOnly` | Boolean default true; sport is still indoor cycling when false. |
|
|
121
|
+
|
|
122
|
+
## Climb course authoring and attribution
|
|
123
|
+
|
|
124
|
+
Prefer a full `course` returned by catalog lookup/import/generation over manually inventing geography. Supported source kinds: `catalog`, `strava_segment`, `strava_route`, `strava_activity`, `gpx`, `road`, `climb_page`, `synthetic`, `manual`. Preserve source provenance and attribution. Workouts store a snapshot, not a dependency on a connected source account.
|
|
125
|
+
|
|
126
|
+
A `course` requires:
|
|
127
|
+
|
|
128
|
+
- `id`, `name`: nonempty strings.
|
|
129
|
+
- `source`: required `kind` enum above; optional string `externalId`, valid URL `url`, string `attribution`, ISO datetime `importedAt`.
|
|
130
|
+
- `totalDistanceM` > 0; `totalAscentM` >= 0; `totalDescentM` >= 0 defaults 0.
|
|
131
|
+
- `averageGradePct`, `maxGradePct`: numbers.
|
|
132
|
+
- `segments`: nonempty array of `{ "distanceM": positiveNumber, "gradePct": numberFromMinus30To35 }`.
|
|
133
|
+
- Optional `region` string, numeric `startElevationM`, `summitElevationM`, `climbCategory`: `hc|1|2|3|4|uncategorized`.
|
|
134
|
+
- Optional `track`: 2..300 points with `distanceM` >= 0, latitude −90..90, longitude −180..180. Keep distances ordered and totals consistent; schema does not cross-check aggregate totals against segments. `buildCourseProfile` computes totals/category and downsamples tracks locally.
|
|
135
|
+
|
|
136
|
+
`pacingPctFtp` is > 0 and <= 3, resolved default **0.85** (not inserted as a schema default). Compiled pacing watts = rounded FTP × fraction. Estimate uses rider mass from `athleteContext.weightKg` or 75 kg, fixed bike mass 7.5 kg, and elevation when available. Legacy bike weights are ignored.
|
|
137
|
+
|
|
138
|
+
Include `grade` in allowed modes (and `erg` for power-target warmup/cooldown). Compiler emits grade mode, clamped mean-grade initial target, segment grades clamped to −20..25%, and a course runtime payload carrying attribution/pacing/physics. The runtime tracks live virtual distance and finishes at the course end, **not** the estimated duration. Unsupported simulation hardware can fall back to pacing-watts ERG in the app; the compiler still requires grade authorization. Changing pacing changes estimated duration/TSS, not the terrain or a guaranteed completion time. Duration constraints on climbs are therefore preview estimates, not time guarantees.
|
|
139
|
+
|
|
140
|
+
Warnings: total distance < 300 m (`climb.course.too_short`), mean grade < 1% (`climb.course.flat`), any segment outside trainer −20..25% (`climb.course.grade_clamped`). Source segment schema still rejects outside −30..35%.
|
|
141
|
+
|
|
142
|
+
## Validation is necessary, not a complete coaching/safety proof
|
|
143
|
+
|
|
144
|
+
Call `validate_workout` with `{ "workout": candidate }`. It returns `validation`, `computed`, and (unless validation fails) `runtimePlanPreview`. Schema errors happen before semantic validation. A fail blocks artifact runtime compilation; calling the low-level compiler alone is not validation.
|
|
145
|
+
|
|
146
|
+
Implemented semantic checks:
|
|
147
|
+
|
|
148
|
+
- No executable steps; global 24-hour / authored max duration exceedance.
|
|
149
|
+
- Intensity above max FTP, based on constant/zone/ramp-mean/climb-pacing intensity.
|
|
150
|
+
- Non-open execution mode absent from allowed modes.
|
|
151
|
+
- Lower of step hard/soft HR caps greater than workout max HR.
|
|
152
|
+
- Standing text detected with `disallowStanding`: regex `\bstand(?:ing)?\b` in executable title/cadence cue/instructions. Even “do not stand” matches; use “stay seated”. Note text is not checked.
|
|
153
|
+
- Missing target warnings only for `steady`, `ramp`, `work`. Explicit `open` does not warn.
|
|
154
|
+
- Continuous work adds consecutive known-intensity steps >= .88 FTP except `recovery`, `cooldown`, `rest`; other steps reset it. This is not simply the duration of an interval set.
|
|
155
|
+
- With `avoidSprintStarts`, known intensity >= 1.2 starting before elapsed 120 seconds is an error except warmup/recovery/cooldown. First known intensity >= 1.2 also warns, even when preceded by open steps.
|
|
156
|
+
- Climb warnings above. No mandatory warmup/cooldown/cadence-drill cue or objective-specific physiology checks.
|
|
157
|
+
|
|
158
|
+
Issue paths use zero-based string indices, e.g. `["blocks","1","steps","0","repeat","2"]`. `describeWorkoutPath` renders “block 2 > step 1 > repeat 3”; do not interpret indices as text line numbers.
|
|
159
|
+
|
|
160
|
+
Metrics: duration sums flattened steps; ramps count wholly in their average-intensity zone, not split over zones. Unknown effort (open/resistance/constant grade) contributes zero modeled power/IF/TSS and z1 time, not evidence of zero actual effort. Climbs use pacing. IF is root-mean-square duration-weighted FTP ratio (rounded 3 decimals), TSS = hours × IF² × 100 (1 decimal); estimated kJ uses duration-weighted mean power. Zone boundaries are <.56, <.76, <.91, <1.06, <1.21, <1.51, then z7.
|
|
161
|
+
|
|
162
|
+
Save recalculates artifacts; `expectedValidationStatus` (`pass|warn|fail`) is an equality guard, not an override. Failing workouts can be saved as `draft` (except >24 hours, rejected); passing/warning workouts save as `validated`, unless retired. Thus require the validated status you reviewed, rather than assuming save rejects all errors. Notes-only candidates also have zero computed duration, which conflicts with the positive-duration output schema: keep an executable step.
|
|
163
|
+
|
|
164
|
+
## Edit → revalidate → save loop
|
|
165
|
+
|
|
166
|
+
1. Keep the complete candidate as `{ "workout": ... }` in `candidate.json`. Edit this definition, not the compiled runtime preview. Unsaved drafts can be edited directly; a saved workout needs a new version (see revisions below).
|
|
167
|
+
2. Remove stale `computed` and `validation` fields after edits. Keep athlete context and constraints intentional: FTP, targets, repetition counts, courses and pacing can all affect validation and load estimates.
|
|
168
|
+
3. Run `wattson --json workout validate --input-file candidate.json > validation.json` (MCP: `validate_workout` with the same object). A successful request/exit code is not a validation pass.
|
|
169
|
+
4. Inspect `validation.status` and every issue's `code`, `path`, `severity` and `message`. Fix schema errors first; locate semantic failures in the authored block/step using the issue path. Repair the actual mismatch, not its status label. Do not loosen a rider's safety constraint simply to obtain a pass.
|
|
170
|
+
5. Repeat validation after **every** edit: the previous result no longer describes the edited candidate. Check `computed.durationSec`, load estimates and `runtimePlanPreview` for the intended order, repetitions, modes and targets. Review warnings; repair failures before treating the workout as rideable.
|
|
171
|
+
6. Construct save input from the exact candidate just reviewed, with `expectedValidationStatus` equal to the reviewed status (`pass` by default). Validation returns artifacts, not a replacement workout. The guard compares status only; it does not bind a previous validation response to the candidate. Save recomputes validation.
|
|
172
|
+
7. For saved-workout edits include `expectedLatestVersion` for the saved base. On conflict, fetch the latest workout, reapply intended edits and validate again; do not blindly change the guard and retry. Fetch the returned saved ID/version and runtime plan to verify persistence.
|
|
173
|
+
|
|
174
|
+
The [command reference](commands.md#revise-a-saved-version) has a guarded revision sequence; the composition example below supplies a complete new-workout sequence. Neither validation nor revision updates an in-progress ride.
|
|
175
|
+
|
|
176
|
+
## Revisions: supported patch and actual limitations
|
|
177
|
+
|
|
178
|
+
`revise_workout` takes `{ "patch": ... }`, not a full workout. Both MCP surfaces register this same name and use `ReviseWorkoutInputSchema`; neither takes `athleteId`. The authenticated surface calls the client/service method `reviseMyWorkout` and checks ownership of the saved base before revision. It loads a **saved** base; use direct JSON edits for an unsaved draft.
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"patch": {
|
|
183
|
+
"workoutId": "saved-id",
|
|
184
|
+
"baseVersion": 1,
|
|
185
|
+
"preserve": ["objective", "interval_structure"],
|
|
186
|
+
"changes": { "newTitle": "Controlled VO2", "intensityMultiplier": 0.95 },
|
|
187
|
+
"naturalLanguage": "Make the efforts a little easier",
|
|
188
|
+
"reason": "Reduce today's load"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Required: nonempty `workoutId`, positive integer `baseVersion`, string `reason`. `naturalLanguage` is optional descriptive text, **not parsed into operations**. `preserve` defaults `[]`; `changes` defaults `{}`. All change fields are optional:
|
|
194
|
+
|
|
195
|
+
| Change | Contract and actual implementation |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `newTitle` | Trimmed nonempty string, max 100 chars. |
|
|
198
|
+
| `newObjective` | Objective enum; label only, does not rebuild structure. |
|
|
199
|
+
| `durationDeltaSec` | Integer; implementation clamps to −600..600, modifies from tail. Negative edits leave changed steps >=30s. Positive edits add to last simple block or last step of last set; repeats multiply the real total change. The helper does not skip notes/climbs: a trailing one has no `durationSec`, so positive edits assign `NaN`; negative edits also poison the remaining subtraction with `NaN`, preventing earlier timed blocks from being shortened. Hand-edit those compositions instead. |
|
|
200
|
+
| `intensityMultiplier` | Schema .7..1.2, implementation clamps to **.8..1.1**. Scales watts/fractions/resistance and ramp endpoints. Zone/open/constant-grade targets unchanged. Climbs scale pacing (clamped .2..3), changing estimated duration too. Other scaled values can exceed target schema limits and make the revision output fail schema parsing (e.g. resistance 100 × 1.1). |
|
|
201
|
+
| `addCadenceFocus` | Boolean; adds min85/max105/cue to non-note top-level blocks without any cadence object, including sets/climbs. Does not replace existing cadence or directly edit child steps. |
|
|
202
|
+
| `removeLastInterval` | Boolean; despite name, current code removes the **last non-interval-set top-level block**, often cooldown/note/climb. It does not remove a repetition. Hand-edit blocks for a real interval deletion. |
|
|
203
|
+
| `switchToRecovery` | Boolean; only rewrites top-level `recovery` targets. Absent target becomes .55 FTP; numeric `value` becomes .55 **in its existing mode** (including watts/resistance/grade). Ramps/zones/open and interval children unchanged; objective unchanged. Not a general recovery transformation. |
|
|
204
|
+
|
|
205
|
+
Preserve semantics:
|
|
206
|
+
|
|
207
|
+
- `objective` suppresses `newObjective` only.
|
|
208
|
+
- `duration` suppresses `durationDeltaSec` only; removal/climb-pacing edits can still change total time.
|
|
209
|
+
- `interval_structure` suppresses `removeLastInterval` and `addCadenceFocus`, not intensity/duration edits.
|
|
210
|
+
- `tss` is accepted and echoed but **not enforced**. Do not promise TSS preservation.
|
|
211
|
+
|
|
212
|
+
Operation order: removal, duration, intensity, cadence, recovery shift, title/objective. A duration change moves `maxDurationSec` only if old cap equalled old computed duration; independent caps remain. Returned diff includes requested intent (not necessarily applied operation), actual final duration and preserve flags. Review candidate blocks and validation, not only diff intent. Revision increments version and timestamp but does not save; save the returned workout with `expectedLatestVersion` set to the base/latest saved version. Direct manual revisions similarly preserve ID, increment version and use the concurrency guard. Retain creation metadata and provide truthful updated attribution.
|
|
213
|
+
|
|
214
|
+
Implementation evidence: MCP names/schemas are in `services/planner-mcp/src/tools.ts` (`registerWriteTools`, `registerPublicWriteTools`). Revision behavior is in `services/planner-api/src/service.ts` (`withRecoveryShift`, `scaleTargetValue`, `withRemovedLastInterval`, `withDurationDelta`, `reviseWorkout`); lineage overwrite is in `saveWorkout`. These are current implementation quirks, not desired coaching semantics.
|
|
215
|
+
|
|
216
|
+
## Full composition example: draft → edit → validate → save
|
|
217
|
+
|
|
218
|
+
This builds an exact 20-minute session: 5-minute warmup, three 2-minute efforts each followed by 2-minute recovery, 3-minute cooldown. Drafting supplies the real athlete/metadata envelope; the script replaces every structural field it needs, without inventing server IDs or computed values. CLI examples assume an authenticated `wattson`; see [commands.md](commands.md) for installation/authentication.
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
wattson workout draft --objective vo2max --duration-min 20 --json > draft.json
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Save this as `compose-workout.mjs` and run `node compose-workout.mjs`:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
228
|
+
const { workout } = JSON.parse(readFileSync('draft.json', 'utf8'));
|
|
229
|
+
if (!workout) throw new Error('Expected draft response with workout');
|
|
230
|
+
workout.title = 'Controlled VO2 20';
|
|
231
|
+
workout.objective = 'vo2max';
|
|
232
|
+
workout.blocks = [
|
|
233
|
+
{ id: 'warmup', type: 'warmup', durationSec: 300,
|
|
234
|
+
target: { mode: 'ramp_pct_ftp', start: 0.55, end: 0.7 },
|
|
235
|
+
instructions: ['Build smoothly'] },
|
|
236
|
+
{ id: 'efforts', type: 'interval_set', repeat: 3,
|
|
237
|
+
cadence: { min: 85, max: 105 }, instructions: ['Stay seated'],
|
|
238
|
+
steps: [
|
|
239
|
+
{ id: 'work', type: 'work', durationSec: 120,
|
|
240
|
+
target: { mode: 'pct_ftp', value: 1.15 },
|
|
241
|
+
instructions: ['Do not sprint the start'] },
|
|
242
|
+
{ id: 'recover', type: 'recovery', durationSec: 120,
|
|
243
|
+
target: { mode: 'pct_ftp', value: 0.5 }, instructions: [] }
|
|
244
|
+
] },
|
|
245
|
+
{ id: 'cooldown', type: 'cooldown', durationSec: 180,
|
|
246
|
+
target: { mode: 'ramp_pct_ftp', start: 0.55, end: 0.45 },
|
|
247
|
+
instructions: ['Ease down gradually'] }
|
|
248
|
+
];
|
|
249
|
+
workout.constraints = {
|
|
250
|
+
...workout.constraints,
|
|
251
|
+
maxDurationSec: 1200, maxPctFtp: 1.2, maxContinuousWorkSec: 120,
|
|
252
|
+
allowedExecutionModes: ['erg'], disallowStanding: true,
|
|
253
|
+
avoidSprintStarts: true, indoorOnly: true
|
|
254
|
+
};
|
|
255
|
+
workout.rationale = {
|
|
256
|
+
summary: 'Three controlled two-minute VO2 efforts with equal recovery.',
|
|
257
|
+
whyToday: ['Explicit 20-minute composition; confirm suitability for the rider.'],
|
|
258
|
+
adaptationGoal: 'Sustained high-aerobic efforts with controlled starts'
|
|
259
|
+
};
|
|
260
|
+
workout.tags = ['vo2max', '20min', 'mode:erg'];
|
|
261
|
+
delete workout.computed;
|
|
262
|
+
delete workout.validation;
|
|
263
|
+
writeFileSync('candidate.json', JSON.stringify({ workout }, null, 2));
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
wattson workout validate --input-file candidate.json --json > validation.json
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Inspect all issues and runtime preview, then prepare a guarded save only for a pass:
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
// Run as a separate .mjs file after validation.
|
|
274
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
275
|
+
const candidate = JSON.parse(readFileSync('candidate.json', 'utf8'));
|
|
276
|
+
const result = JSON.parse(readFileSync('validation.json', 'utf8'));
|
|
277
|
+
if (result.validation.status !== 'pass') {
|
|
278
|
+
throw new Error(JSON.stringify(result.validation));
|
|
279
|
+
}
|
|
280
|
+
if (result.computed.durationSec !== 1200 || result.runtimePlanPreview.steps.length !== 8) {
|
|
281
|
+
throw new Error('Unexpected compilation');
|
|
282
|
+
}
|
|
283
|
+
writeFileSync('save.json', JSON.stringify({
|
|
284
|
+
workout: candidate.workout, expectedValidationStatus: 'pass'
|
|
285
|
+
}, null, 2));
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
wattson workout save --input-file save.json --json
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
This final command persists to the selected account; validation does not. For a revision, add `expectedLatestVersion` to save input. Never save merely to test documentation. The example was checked locally against the schema/compiler and an isolated in-memory planner service, without production writes.
|