incremnt 0.8.8 → 0.8.9
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/README.md +1 -1
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/contract.js +26 -9
- package/src/program-draft.js +87 -25
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ incremnt login --session-file ~/Downloads/session.json
|
|
|
110
110
|
| `coach observations list` | Current persisted coach observations |
|
|
111
111
|
| `coach observations seen --id <id>` | Mark a coach observation as seen |
|
|
112
112
|
| `coach observations dismiss --id <id>` | Dismiss an observation and suppress matching follow-ups |
|
|
113
|
-
| `programs create --file <file>` | Create an inactive saved program from generated-plan JSON |
|
|
113
|
+
| `programs create --file <file>` | Create an inactive saved program from generated-plan JSON, including day-local supersets |
|
|
114
114
|
| `programs propose --file <file>` | Submit a program proposal |
|
|
115
115
|
| `programs proposals` | List proposals |
|
|
116
116
|
| `programs proposal dismiss --id <id>` | Dismiss a proposal |
|
package/SKILL.md
CHANGED
|
@@ -26,7 +26,7 @@ Append `--dry-run` to any mutating command to preview the HTTP request as `{ dry
|
|
|
26
26
|
|
|
27
27
|
| Command | Reversibility | Notes |
|
|
28
28
|
|---|---|---|
|
|
29
|
-
| `programs create --file <f>` | Reversible in iOS (created inactive) | Creates a saved inactive program from Coach-compatible generated-plan JSON
|
|
29
|
+
| `programs create --file <f>` | Reversible in iOS (created inactive) | Creates a saved inactive program from Coach-compatible generated-plan JSON, including day-local supersets via `supersetKey`/`supersetOrder`. It does not activate or replace the current plan. Use `records` or `exercises history` first for canonical names. |
|
|
30
30
|
| `programs propose --file <f>` | Reversible (proposal stays pending) | Weights omitted; iOS computes from history. Names must come from `records` or `exercises history`. |
|
|
31
31
|
| `programs proposals` | Read-only lookup | Use before dismissing a proposal. |
|
|
32
32
|
| `programs proposal dismiss --id <id>` | Irreversible | IDs come from `programs proposals`. |
|
package/package.json
CHANGED
package/src/contract.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const contractVersion =
|
|
1
|
+
export const contractVersion = 25;
|
|
2
2
|
|
|
3
3
|
export const capabilities = {
|
|
4
4
|
readOnly: false,
|
|
@@ -281,7 +281,7 @@ export const writeCommandSchema = [
|
|
|
281
281
|
description: 'Create a saved inactive program from a generated plan file',
|
|
282
282
|
usage: 'programs create --file <file>',
|
|
283
283
|
dryRun: true,
|
|
284
|
-
agentNotes: 'Mutation. Creates a brand-new inactive saved program from Coach-compatible generated-plan JSON; it does not activate or replace the current plan. External assistants should draft the JSON file, then run programs create --file <path>. Use records or exercises history before drafting exercise names; do not invent unusual exercise names.',
|
|
284
|
+
agentNotes: 'Mutation. Creates a brand-new inactive saved program from Coach-compatible generated-plan JSON; it does not activate or replace the current plan. External assistants should draft the JSON file, then run programs create --file <path>. Use records or exercises history before drafting exercise names; do not invent unusual exercise names. Supersets are supported with day-local supersetKey plus supersetOrder; the service converts them to native iOS supersetGroupId fields.',
|
|
285
285
|
options: [
|
|
286
286
|
{ name: 'file', type: 'string', format: 'path', required: true, description: 'Path to Coach-compatible generated-plan JSON file' }
|
|
287
287
|
]
|
|
@@ -446,31 +446,48 @@ for (const command of writeCommandSchema) {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
export const programDraftSchema = {
|
|
449
|
-
description: 'JSON structure for programs create --file and programs propose --file.
|
|
449
|
+
description: 'JSON structure for programs create --file and programs propose --file. This mirrors the current iOS program shape: concrete set reps, optional warmups/notes/RIR, and day-local supersetKey groups. Rep ranges and rest overrides are not supported.',
|
|
450
450
|
required: ['name', 'daysPerWeek', 'equipmentTier', 'days'],
|
|
451
451
|
properties: {
|
|
452
452
|
name: { type: 'string', description: 'Program name' },
|
|
453
453
|
equipmentTier: { type: 'string', enum: ['fullGym', 'benchDumbbells', 'dumbbellsOnly', 'bodyweightOnly'] },
|
|
454
454
|
daysPerWeek: { type: 'integer', minimum: 1, maximum: 7, description: 'Training days per week' },
|
|
455
|
+
volumeLevel: { type: 'string', enum: ['minimum', 'moderate', 'high'], description: 'Program volume label. Optional, defaults to moderate.' },
|
|
456
|
+
currentDayIndex: { type: 'integer', minimum: 0, description: 'Initial current day index. Optional, defaults to 0.' },
|
|
455
457
|
trainingWeekdays: { type: 'array', items: { type: 'integer', minimum: 0, maximum: 6 }, description: 'Days of week (0=Sun). Optional.' },
|
|
456
|
-
rationale: { type: 'string', description: 'Why this program was suggested. Optional.' },
|
|
457
458
|
days: {
|
|
458
459
|
type: 'array',
|
|
459
460
|
minItems: 1,
|
|
460
461
|
items: {
|
|
461
|
-
required: ['title', 'exercises'],
|
|
462
|
+
required: ['dayLabel', 'title', 'exercises'],
|
|
462
463
|
properties: {
|
|
464
|
+
dayLabel: { type: 'string', description: 'Short day label, e.g. "Day 1".' },
|
|
463
465
|
title: { type: 'string', description: 'Day name, e.g. "Upper A"' },
|
|
466
|
+
subtitle: { type: 'string', description: 'Optional day subtitle.' },
|
|
464
467
|
exercises: {
|
|
465
468
|
type: 'array',
|
|
466
469
|
minItems: 1,
|
|
467
470
|
items: {
|
|
468
|
-
required: ['name', '
|
|
471
|
+
required: ['name', 'muscleGroup', 'sets'],
|
|
469
472
|
properties: {
|
|
470
473
|
name: { type: 'string', description: 'Exercise name (use canonical names from records or exercises history)' },
|
|
471
|
-
muscleGroup: { type: 'string', description: 'e.g. "Chest", "Back".
|
|
472
|
-
sets: {
|
|
473
|
-
|
|
474
|
+
muscleGroup: { type: 'string', description: 'e.g. "Chest", "Back".' },
|
|
475
|
+
sets: {
|
|
476
|
+
type: 'array',
|
|
477
|
+
minItems: 1,
|
|
478
|
+
items: {
|
|
479
|
+
required: ['weight', 'reps'],
|
|
480
|
+
properties: {
|
|
481
|
+
weight: { type: 'number', minimum: 0, description: 'Planned load. Use 0 for bodyweight or app-computed/manual adjustment later.' },
|
|
482
|
+
reps: { type: 'integer', minimum: 1, maximum: 30 },
|
|
483
|
+
isWarmup: { type: 'boolean', description: 'Optional warmup marker.' }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
note: { type: 'string', description: 'Optional prescription note.' },
|
|
488
|
+
rir: { type: 'integer', minimum: 0, maximum: 5, description: 'Optional app-supported RIR field.' },
|
|
489
|
+
supersetKey: { type: 'string', description: 'Optional day-local label shared by 2+ exercises to create a native iOS superset.' },
|
|
490
|
+
supersetOrder: { type: 'integer', minimum: 0, maximum: 24, description: 'Required when supersetKey is present; order within that superset group.' }
|
|
474
491
|
}
|
|
475
492
|
}
|
|
476
493
|
}
|
package/src/program-draft.js
CHANGED
|
@@ -18,12 +18,15 @@ export const PROGRAM_DRAFT_LIMITS = {
|
|
|
18
18
|
dayTitleMaxLen: 120,
|
|
19
19
|
daySubtitleMaxLen: 120,
|
|
20
20
|
noteMaxLen: 1000,
|
|
21
|
+
supersetKeyMaxLen: 64,
|
|
21
22
|
minWeight: 0,
|
|
22
23
|
maxWeight: 600,
|
|
23
24
|
minReps: 1,
|
|
24
25
|
maxReps: 30,
|
|
25
26
|
minRir: 0,
|
|
26
27
|
maxRir: 5,
|
|
28
|
+
minSupersetOrder: 0,
|
|
29
|
+
maxSupersetOrder: 24,
|
|
27
30
|
minSetsPerExercise: 1,
|
|
28
31
|
maxSetsPerExercise: 12,
|
|
29
32
|
minExercisesPerDay: 1,
|
|
@@ -81,7 +84,7 @@ function normalizeProgramDraftSet(set) {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
function normalizeProgramDraftExercise(exercise, canonicalizeExerciseName, strict = false) {
|
|
84
|
-
if (!hasOnlyAllowedKeys(exercise, new Set(['name', 'muscleGroup', 'sets', 'rir', 'note']))) return null;
|
|
87
|
+
if (!hasOnlyAllowedKeys(exercise, new Set(['name', 'muscleGroup', 'sets', 'rir', 'note', 'supersetKey', 'supersetOrder']))) return null;
|
|
85
88
|
|
|
86
89
|
const name = normalizedExerciseDisplayName(exercise?.name, canonicalizeExerciseName);
|
|
87
90
|
const muscleGroup = String(exercise?.muscleGroup ?? '').trim();
|
|
@@ -109,6 +112,20 @@ function normalizeProgramDraftExercise(exercise, canonicalizeExerciseName, stric
|
|
|
109
112
|
const note = exercise?.note == null ? null : String(exercise.note);
|
|
110
113
|
if (note && note.length > PROGRAM_DRAFT_LIMITS.noteMaxLen) return null;
|
|
111
114
|
|
|
115
|
+
const supersetKey = exercise?.supersetKey == null ? null : String(exercise.supersetKey).trim();
|
|
116
|
+
if (supersetKey != null && (
|
|
117
|
+
!supersetKey ||
|
|
118
|
+
supersetKey.length > PROGRAM_DRAFT_LIMITS.supersetKeyMaxLen
|
|
119
|
+
)) return null;
|
|
120
|
+
|
|
121
|
+
const supersetOrder = exercise?.supersetOrder == null ? null : Number(exercise.supersetOrder);
|
|
122
|
+
if (supersetKey == null && supersetOrder != null) return null;
|
|
123
|
+
if (supersetKey != null && (
|
|
124
|
+
!Number.isInteger(supersetOrder) ||
|
|
125
|
+
supersetOrder < PROGRAM_DRAFT_LIMITS.minSupersetOrder ||
|
|
126
|
+
supersetOrder > PROGRAM_DRAFT_LIMITS.maxSupersetOrder
|
|
127
|
+
)) return null;
|
|
128
|
+
|
|
112
129
|
return {
|
|
113
130
|
name,
|
|
114
131
|
muscleGroup,
|
|
@@ -116,10 +133,33 @@ function normalizeProgramDraftExercise(exercise, canonicalizeExerciseName, stric
|
|
|
116
133
|
nextSuggestion: '',
|
|
117
134
|
sets,
|
|
118
135
|
...(note ? { note } : {}),
|
|
119
|
-
...(rir != null ? { rir } : {})
|
|
136
|
+
...(rir != null ? { rir } : {}),
|
|
137
|
+
...(supersetKey != null ? { supersetKey, supersetOrder } : {})
|
|
120
138
|
};
|
|
121
139
|
}
|
|
122
140
|
|
|
141
|
+
function hasValidDaySupersets(exercises) {
|
|
142
|
+
const grouped = new Map();
|
|
143
|
+
for (const exercise of exercises) {
|
|
144
|
+
if (!exercise.supersetKey) continue;
|
|
145
|
+
const existing = grouped.get(exercise.supersetKey) ?? [];
|
|
146
|
+
existing.push(exercise);
|
|
147
|
+
grouped.set(exercise.supersetKey, existing);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const group of grouped.values()) {
|
|
151
|
+
if (group.length < 2) return false;
|
|
152
|
+
const orders = new Set();
|
|
153
|
+
for (const exercise of group) {
|
|
154
|
+
if (!Number.isInteger(exercise.supersetOrder)) return false;
|
|
155
|
+
if (orders.has(exercise.supersetOrder)) return false;
|
|
156
|
+
orders.add(exercise.supersetOrder);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
123
163
|
function normalizeProgramDraftDay(day, canonicalizeExerciseName, strict = false) {
|
|
124
164
|
if (!hasOnlyAllowedKeys(day, new Set(['dayLabel', 'title', 'subtitle', 'exercises']))) return null;
|
|
125
165
|
|
|
@@ -139,6 +179,7 @@ function normalizeProgramDraftDay(day, canonicalizeExerciseName, strict = false)
|
|
|
139
179
|
exercises.length < PROGRAM_DRAFT_LIMITS.minExercisesPerDay ||
|
|
140
180
|
exercises.length > PROGRAM_DRAFT_LIMITS.maxExercisesPerDay
|
|
141
181
|
) return null;
|
|
182
|
+
if (!hasValidDaySupersets(exercises)) return null;
|
|
142
183
|
|
|
143
184
|
return { dayLabel, title, subtitle, exercises };
|
|
144
185
|
}
|
|
@@ -219,10 +260,15 @@ function defaultChildIdFactory() {
|
|
|
219
260
|
return randomUUID();
|
|
220
261
|
}
|
|
221
262
|
|
|
263
|
+
function defaultSupersetIdFactory() {
|
|
264
|
+
return randomUUID();
|
|
265
|
+
}
|
|
266
|
+
|
|
222
267
|
export function buildProgramFromDraft(rawProgram, {
|
|
223
268
|
now = new Date(),
|
|
224
269
|
idFactory = defaultProgramIdFactory,
|
|
225
|
-
childIdFactory = defaultChildIdFactory
|
|
270
|
+
childIdFactory = defaultChildIdFactory,
|
|
271
|
+
supersetIdFactory = defaultSupersetIdFactory
|
|
226
272
|
} = {}) {
|
|
227
273
|
const draft = normalizeProgramDraft(rawProgram, { strict: true });
|
|
228
274
|
if (!draft) {
|
|
@@ -230,28 +276,44 @@ export function buildProgramFromDraft(rawProgram, {
|
|
|
230
276
|
}
|
|
231
277
|
|
|
232
278
|
const createdAt = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
|
|
233
|
-
const days = draft.days.map((day) =>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
279
|
+
const days = draft.days.map((day) => {
|
|
280
|
+
const supersetGroupIds = new Map();
|
|
281
|
+
const groupIdForKey = (key) => {
|
|
282
|
+
if (!supersetGroupIds.has(key)) {
|
|
283
|
+
supersetGroupIds.set(key, supersetIdFactory('superset'));
|
|
284
|
+
}
|
|
285
|
+
return supersetGroupIds.get(key);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
id: childIdFactory('day'),
|
|
290
|
+
dayLabel: day.dayLabel,
|
|
291
|
+
title: day.title,
|
|
292
|
+
subtitle: day.subtitle,
|
|
293
|
+
exercises: day.exercises.map((exercise) => ({
|
|
294
|
+
id: childIdFactory('exercise'),
|
|
295
|
+
name: exercise.name,
|
|
296
|
+
muscleGroup: exercise.muscleGroup,
|
|
297
|
+
lastSuggestion: exercise.lastSuggestion,
|
|
298
|
+
nextSuggestion: exercise.nextSuggestion,
|
|
299
|
+
sets: exercise.sets.map((set) => ({
|
|
300
|
+
id: childIdFactory('set'),
|
|
301
|
+
weight: set.weight,
|
|
302
|
+
reps: set.reps,
|
|
303
|
+
isComplete: false,
|
|
304
|
+
isWarmup: set.isWarmup === true
|
|
305
|
+
})),
|
|
306
|
+
...(exercise.note ? { note: exercise.note } : {}),
|
|
307
|
+
...(exercise.rir != null ? { rir: exercise.rir } : {}),
|
|
308
|
+
...(exercise.supersetKey
|
|
309
|
+
? {
|
|
310
|
+
supersetGroupId: groupIdForKey(exercise.supersetKey),
|
|
311
|
+
supersetOrder: exercise.supersetOrder
|
|
312
|
+
}
|
|
313
|
+
: {})
|
|
314
|
+
}))
|
|
315
|
+
};
|
|
316
|
+
});
|
|
255
317
|
|
|
256
318
|
return {
|
|
257
319
|
id: idFactory('program'),
|