feat!: autonomous, faithful session detail (program/free/rowing) + today one-shot (#23)#24
Merged
Merged
Conversation
…on (#23) `session <id> --json` reported the *planned* weight, not what was lifted: for a completed program session the API leaves each rep's top-level `weight` null, so the CLI fell back to the exercise `maxWeight` and stamped it on every set — fabricating flat, wrong per-set weights that looked logged. It also discarded the richest data the API returns (per-rep, per-side power/ROM/tempo + form scores) living in a nested `trainingInfoDetail` the loader never read. - Stop using `maxWeight` as the per-set weight. Derive the scalar from the real per-rep telemetry: `mean(weights[])` (already per-attachment, so correct for both dual-handle and single-rope with no handle-count branch), falling back to `capacity/reps`, else `0.0`. - Add a `weight_source` marker (`actual` | `derived_avg` | `unavailable`) so a consumer can never mistake a derived average for a logged weight — the anti-corruption guard for this exact bug. - Always emit per-set `capacity` (was parsed but dropped). - Add an opt-in `--telemetry` flag that surfaces per-rep, per-side arrays (weights, watts, amplitude, rope speed, finished/break times, timestamp), per-set `weight_avg_per_handle`, and per-exercise `scores`/`max_weight`/ `max_weight_count`. Single-attachment moves omit the right-side fields. Guards: TestSessionWeightNeverUsesMaxWeight (negative-assertion regression — a null per-rep weight must never become the planned maxWeight), plus TestSessionTelemetryExposesPerRepDetail and the CLI-level TestSessionTelemetryJSON. Docs (SKILL.md/AGENTS.md/README.md/GOAL.md §9.3) updated to keep advertised == actual. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
… derived fields)
Supersedes the telemetry-flag design earlier on this branch. `session <id>` is now
a dumb, complete, lossless dump of the session's Speediance data: it fetches BOTH
session endpoints and emits each one's data payload verbatim, with the original
Speediance field names and values. The CLI interprets, reshapes, renames, computes,
and fabricates nothing — consumers decide shape, derivation, and storage.
- Output is `{training_id, info, detail}` where info = the raw cttTrainingInfo data
(incl. completionRate) and detail = the raw cttTrainingInfoDetail data. Both are
carried as json.RawMessage so any field — modeled or not, present today or added
by a future app update — flows straight through (the lossy-typed-struct pattern
is exactly what caused the issue #23 bug).
- Removed: the --telemetry flag, the reps_detail reshape + snake_case renames, the
synthesized per-set weight, weight_source/derived_avg, and weight_avg_per_handle.
No flag "unlocks" data — the endpoints return it, so the CLI returns it.
- Never fabricates: a field/array Speediance omits is omitted; a session with no
detail emits "detail": null. Both endpoints are fetched, so completionRate is no
longer lost.
- Real per-rep weights remain available verbatim in trainingInfoDetail.weights[]
(already per-attachment) — consumers average if they want a single number.
api.FetchSessionDetail replaces FetchDetail/PopulateDetail; the typed session
output (SetData/SetOut/ExerciseOut/Session/RepDetail/Scores, AddDetailSets,
SessionOutput, and their helpers) is deleted. Net diff is ~415 lines smaller.
Tests: the genuine captured 940759 response is kept as the fixture; new guards
assert the raw keys pass through unrenamed (leftBreakTimes, forceControlScore,
bilateralBalanceScore, amplitudeStableScore, leftWatts/rightWatts, weights, …), the
sparse set emits only what was returned, nil payloads serialize as null, no
derived/renamed field appears, and the --telemetry flag no longer exists. workouts
is unchanged. Docs (README/SKILL/AGENTS/GOAL §9.3) updated.
BREAKING CHANGE: session --json now emits {training_id, info, detail} with verbatim
Speediance field names. The previous {training_id, completion_rate, exercises[...]}
shape, the derived weight/weight_source fields, and the --telemetry flag are removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
…ee/rowing)
The tool now resolves a session's type ITSELF, so a calling agent never needs to
know what the client did. "Client completed a workout — give me the data" just
works whether it was a program/Coach session, free weights, or a rowing/ski free
session.
- session <id> is autonomous: it probes the program namespace
(cttTrainingInfo[Detail]) and falls back to the free namespace
(freeTraining[Detail], discovered by exploring the live API), then emits a
uniform {training_id, kind, info, detail} with kind ∈ {program, free, ""}.
info/detail remain the VERBATIM Speediance payloads — no derivation.
- New `today [--date today|yesterday|YYYY-MM-DD]` command: the one-shot,
agent-friendly entry point. It lists the day's sessions and returns each fully
resolved as an array of the same shape. Because the list carries the
authoritative numeric `type` alongside the trainingId, dispatch is collision-free.
- A trainingId collides across namespaces (the same number is unrelated sessions
in program vs free), so `--free` / `--program` strict overrides force a namespace
when an id is ambiguous; they are selection flags only (no content reshaping).
- workouts rows gain a `kind` field (mapped from the numeric type) for filtering.
Free lifts (incl. rowing/ski) expose session-level totals in info
(totalCapacity/totalEnergy/totalDistance/trainingCount) and an empty detail —
that is all Speediance records for them; there is no per-rep breakdown.
internal/api: ResolveSession + DispatchMode (Auto/FreeFirst/Program/FreeOnly),
program/free probes, FetchDay, fetchRecords refactor. internal/workout:
SessionType + KindForType, kind on Summary and SessionDetail. Tests cover
auto-detect free, today across mixed types, the collision overrides, mutually
exclusive flags, no-data-anywhere, and that program passthrough stays verbatim.
Docs (README/SKILL/AGENTS/GOAL §6,§9.3) updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
…y|null) Answering a consumer-agent question (advertised==actual): document that info is object|null and detail is array|null, both never normalized, so consumers treat null and [] identically as "no rows". Behavior is unchanged and already covered by TestSessionAutoDetectsFree (detail []), TestSessionNoDataAnywhere / TestSessionDetailNilPayloadsAreNull (null), and TestSessionFaithfulPassthroughJSON. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
…ll detail Discovered while verifying a real "Aerobic Rowing" session (2073939): guided free-namespace sessions (numeric type 2/7 — guided cardio like Aerobic Rowing, and guided programs) are served by freeTraining[Detail], and unlike a freestyle Free Lift their freeTrainingDetail is POPULATED — Aerobic Rowing carries 10 per-interval finishedReps (distance/pace/spm/time/maxHeartRate) plus per-stroke left/rightMinRopeLengths traces (~241 samples/interval). The autonomous resolver already delivered these (free fallback), but the digest mislabeled them: - KindForType: map every positive non-5 type to "free" (was only 1), so `workouts`/`today` label guided rowing/programs "free" instead of "" — matching what `session`/`today` resolve by endpoint. type 5 stays "program"; 0/absent "". - modeForType: non-5 types now dispatch free-first (efficient + collision-safe); type 5 stays program-first. Both still fall back, so a misclassification resolves. kind="free" therefore means the free *namespace*, not "freestyle": it spans a freestyle Free Lift (no info.name, detail []) AND a guided session (info.name, often a populated detail). Docs (SKILL/AGENTS) updated to say so and to stop implying free ⇒ empty detail. Tests: TestKindForType, a guided-rowing fixture in dispatchServer (type 7, populated freeTrainingDetail), TestSessionGuidedRowingHasDetail, and today now resolves the rowing session with its per-interval data in one call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
…owing split) Consumer-agent flagged that a rowing interval's `pace` field is an instantaneous sample, not distance/time. Per the faithful philosophy the CLI emits it verbatim and never "corrects" it — document that Speediance fields aren't guaranteed self-consistent and consumers should derive (split = distance/time). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzdULPGPuCwAiWsBNddadF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #23. Breaking change to the
session --jsoncontract.sessionis now a faithful, verbatim passthrough and autonomous: given only atraining_idit figures out what the session was — a program/Coach session, free weights, or a rowing/ski free session — and emits the raw Speediance payloads under one uniform shape. A newtodaycommand makes "give me the client's workout for today" a single call. The calling agent never needs to know the session type.Uniform output —
{training_id, kind, info, detail}kind∈"program"|"free"|"", and tells a type-agnostic consumer how to read the two verbatim payloads:kindinfodetailprogramcttTrainingInfo(incl.completionRate)cttTrainingInfoDetail— per-exercise + per-rep arraysfreefreeTrainingtotals (totalCapacity,totalEnergy,totalDistancefor rowing/ski,trainingCount)freeTrainingDetail— usually[](free lifts have no per-rep data)""nullnull(no session in either namespace)info/detailare the endpoints'datapayloads verbatim — original field names and values (leftWatts,forceControlScore,weights,leftBreakTimes, …), carried as raw JSON so any field (now or after a future app update) flows straight through. No renaming, no derivation, no synthesized weight, no--telemetry. Absence is preserved (omitted fields stay omitted).Autonomous dispatch (the headline)
session <id>probes the program namespace, falls back to the free namespace (freeTraining/freeTrainingDetail, discovered by exploring the live API), and reports which answered viakind.today [--date today|yesterday|YYYY-MM-DD]— one call returns every session on the day, each fully resolved (array of the same shape). It reads each record's authoritative numerictypefrom the list, so dispatch is collision-free.--free/--programare strict overrides: atrainingIdcan identify different sessions in the program vs. free namespaces, so these force one when an id is ambiguous (selection only — no content reshaping).workoutsrows gain akindfield for filtering.Live-verified on a real account
Why free lifts look sparse
Exploring the API showed Free Lifts (incl. rowing/ski) live at
freeTraining/<id>and expose session-level totals only —freeTrainingDetailis[]andactionListis empty/absent even for a real weight free-lift. There is no per-rep breakdown server-side to deliver; the tool surfaces everything that exists.Tests
Auto-detect free;
todayacross mixed types in one day; collision overrides (--free/--programforce the intended session); mutually-exclusive flags; no-data-anywhere →kind:"", nulls; program passthrough stays verbatim (all raw keys present, sparse set preserved, no derived/renamed fields, no--telemetry);workoutsexposeskind; nil payloads →null.go build/vet/test -racegreen;gofmt -lclean;golangci-lint0 issues.session --jsonnow emits{training_id, kind, info, detail}(was{training_id, completion_rate, exercises[...]}originally, then{training_id, info, detail}). The derivedweight/weight_sourceand the--telemetryflag are gone;info/detailare verbatim and polymorphic bykind. New:todaycommand,--free/--programflags, and akindfield onworkoutsrows.🤖 Generated with Claude Code