runlab-sync
Fetch new runs into the local cache and report what actually arrived — counts, coverage per channel, gaps and duplicates. Use when the user says "sync my runs", "import the latest activities", "update
SKILL.md.claude/skills/runlab-sync/SKILL.md
GitHubrunlab sync
Fetching is the easy half. The half that matters is telling the athlete what did not arrive, because a silently truncated import turns into a wrong finding three weeks later.
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 — do not fetch or parse the source by hand.
Step 1 — fetch
bash "${CLAUDE_PLUGIN_ROOT}/.claude/hooks/rl-python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/sync.py"
Options: --source <name> to pick one configured source, --since YYYY-MM-DD to
limit the window, --all to rebuild from scratch, --limit <n> for a probe,
--dry-run to see what would happen, --json for the machine-readable report.
Without --since the script asks every source for everything and skips what the
cache already holds; it does not re-download what it already has, and --since
never removes older activities from the index.
Exit codes: 0 finished and every assurance held, 1 finished but something is
wrong, 2 could not start (no data root, no credentials, no readable directory).
On 1 and 2 the output carries reason and hint — relay the hint, it names
the missing thing.
An exit code of 1 has two causes and they read differently:
reason: partial_import— a source delivered less than it holds. Name the source and its own reason.reason: assurance_violated— the import ran but the cache is not in a state worth analysing. Readassurances[], name every entry whosestatusisviolatedtogether with its numbers, and say that analyses should wait. A shrinking activity count, an activity that claims samples and has none, a channel whose coverage collapsed, or a missing raw payload each invalidate results computed afterwards. Never summarise this as "sync finished".
status: skipped on an assurance is not a pass. It means the check could not
run — usually because this is the first sync and there is no baseline yet. Say
so rather than counting it as green.
This skill writes only into cache/. It never touches state/ or
profile.json; a PreToolUse hook enforces that.
Step 2 — confirm what landed
bash "${CLAUDE_PLUGIN_ROOT}/.claude/hooks/rl-python.sh" "${CLAUDE_PLUGIN_ROOT}/scripts/read-db.py" \
--section computed.activities --section computed.data_quality --section computed.gates
| Where | What it settles |
|---|---|
computed.activities.n_running, .first_date, .last_date, .span_days |
how much history is now in the cache |
computed.activities.n_unusable_entries, .n_undated |
entries that arrived but cannot be used |
computed.activities.index.age_days |
how fresh the cache is |
computed.data_quality.channels[] |
per channel: n, n_total, coverage |
computed.gates |
which analyses the new data unlocked, and what the rest are short of |
The sync's own report answers what read-db.py cannot, because it knows what was
asked for and not delivered. Read these blocks out of it:
| Where | What it settles |
|---|---|
sources[].accounting |
files seen against activities built — the reference folder yields 35 activities from 104 files, and only this block says why |
sources[].placeholders |
iCloud stubs that were skipped; the athlete believes those runs are imported |
coverage.summary / coverage.running |
per channel, n of n_total, for everything and for the runs alone |
streamless.n_structural / .n_failed |
recordings that carry no samples versus fetches that failed — never add these together |
duplicates.decisions[] |
which version of a run was kept, which was dropped, and on what evidence |
gaps.gaps[], gaps.empty_months |
holes in the time series, including a month the export skipped entirely |
Rules for reporting it:
- Always give counts as n of n_total. "Mechanics missing in 12 runs" is unusable; "12 of 76" is a fact. The block gives you both — use both.
- A gap in the calendar is not a training gap. It may be an import problem, a rest week, or an injury. State it as a data observation and ask.
- Failures and unusable entries are never rounded away, even when the count is small. Name the ids and the reason codes.
- If mechanics coverage is 0 and the configured source is GPX or TCX, say that this is the source's ceiling, not a device fault.
- If coverage dropped against the previous sync, say so plainly: a watch that stopped delivering mechanics after an update looks exactly like a form change to every analysis that follows.
After a sync
- Nothing goes into the state database from here. A sync produces facts about the cache, not findings about the athlete.
- If the athlete wants the new runs interpreted, hand over to
/runlab:runlab-runfor a single run or/runlab:runlab-reportfor the overview.
Do not
- Do not retry a failed source in a loop. One retry, then report.
- Do not fill a gap by interpolation, in the cache or in your answer.
- Do not quote a number that is not in this script's output.