runlab-run
Analyse one run — pacing, efficiency decay, decoupling, mechanics, and how it compares with the athlete's own history at the same pace. Use when the user asks "how was today's run", "analyse my long r
SKILL.md.claude/skills/runlab-run/SKILL.md
GitHubrunlab single run
One run is a small sample. Everything this skill says has to survive that fact.
Standing rule
Run every script through the shim, and check --help before relying on a flag.
If a script is absent or its interface differs from what is written here, say so
and stop. Never compute the number yourself.
Step 1 — identify the run
If the user named a date or "today", resolve it to an activity id:
bash "${CLAUDE_PLUGIN_ROOT}/.claude/hooks/rl-python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/read-db.py" --section computed.activities
read-db.py always prints JSON — there is no --json flag. Ids are
adapter-scoped and stable, for example runalyze:900000001 or fit:9c1e.... If
two activities match one date, ask which; do not pick the longer one.
Check computed.gates in the same call when the question needs a comparison:
a gate with a non-empty missing[] tells you the answer before you run anything,
and it gives the shortfall to quote.
Step 2 — run the analysis
bash "${CLAUDE_PLUGIN_ROOT}/.claude/hooks/rl-python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/analyze.py" run --activity <id>
Individual tools, when the question is narrower:
| Tool | Question it answers |
|---|---|
decay |
Does the efficiency index fall over moving time, and how fast in %/h? |
pacing |
Start pattern, variability, negative split, stop time |
compare |
How does this run sit against the athlete's own runs at the same pace? |
mechanics |
Ground contact, vertical oscillation, step length, and their coverage |
week |
The week this run belongs to: volume, load, monotony, ramp |
patterns |
How often each catalogue pattern occurs across many runs |
history |
The whole history: eras, pace and efficiency trends, durability |
eras |
Where the training eras begin and end, and why |
run, decay, compare, mechanics and pacing take the activity; patterns,
week, history and eras take a period instead. Each writes
cache/results/<result_id>.json and prints the id. Quote the id when you state
one of its numbers.
The output is JSON on stdout and nothing else. metrics and coverage are
exactly what the file named by result_id contains; anything else the tool
computed is under detail, beside the id in result_ids that vouches for it.
So cite the id that actually holds the number, not the one printed last.
Step 3 — read the result correctly
verdict.codeis the machine answer:insufficient_data,not_applicable,inconclusive,within_expected,above_expected,below_expected,improving,worsening,stable,confirmed,refuted.insufficient_datais a real answer and is reported as one, with thenthat was available and thenthat would be needed.coveragedecides how much weight a mean carries. A ground contact mean over 40 % of samples and one over 96 % are not the same measurement, and the difference is never averaged away.- The remainder block. Decay is computed in five-minute blocks from minute ten of moving time. The final short block is reported separately because it can move the figure by a wide margin — a walk at the end is not a decay of running economy. State both numbers or neither.
- Moving time, not elapsed time. Pace is computed on moving time. If
stop_time_excessshows up, the athlete's watch average and this analysis will disagree, and the reason is worth one sentence. - Comparison needs a base.
comparereturnsverdict: insufficient_datawhen too few neighbouring blocks exist. That is not a bad run, it is a young database.
Step 4 — what to say
Three parts, in this order:
- What the run was: distance, moving time, pace, heart rate — all from the result file.
- What is unusual about it, if anything, with the comparison that makes it
unusual and the
nof that comparison. - What follows for the next session — or explicitly nothing. "This run gives no reason to change anything" is a complete and often correct answer.
Step 5 — offer to record a pattern
Only if the run shows something a single run can show: a pattern occurrence, not
a finding about the athlete. Patterns use the closed catalogue (zone3_excess,
start_too_fast, cadence_below_target, ...); an unknown id is rejected by the
validator with the valid list.
Propose the payload, show it, dry-run it, then write:
bash "${CLAUDE_PLUGIN_ROOT}/.claude/hooks/rl-python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/update-db.py" \
--payload-file payload.json --dry-run
bash "${CLAUDE_PLUGIN_ROOT}/.claude/hooks/rl-python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/update-db.py" \
--payload-file payload.json
The payload also goes in on stdin when you have no file to spare. It holds no
numbers: it cites result_id and metric_keys, and the writer copies the values
out of the result file. See /runlab:runlab-methodology.
Do not
- Do not turn one run into a trend. A single run cannot establish a pattern; it can only be an observation of one.
- Do not attribute a slow run to fatigue, heat or sleep unless a result file actually tested it. "It was warm" is a hypothesis, and it belongs in the hypothesis wording.
- Do not write into
state/with Write or Edit. The hook blocks it, and the block is the point.