diff --git a/CHANGELOG.md b/CHANGELOG.md
index 802aafe..bc3d4dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to Workout Lens are documented here.
+## [1.5.8] — 2026-05-15
+
+### Developer / Infrastructure
+- **CI guard for `RECS_PROMPT_VERSION` sync (issue #241)** — `RECS_PROMPT_VERSION` is defined independently in `app/src/lib/prompts.js` and `app/api/recsCacheCleanup.js`. A new test (`app/api/__tests__/recsVersion.test.js`) reads both files via regex and fails the suite if the values drift. Forgetting to bump the cleanup job when changing the prompt version caused stale cache entries to never be swept.
+
## [1.5.7] — 2026-05-15
### Security
diff --git a/CLAUDE.md b/CLAUDE.md
index d0ef69a..601d3c4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -263,7 +263,7 @@ recommendation_cache
- **Shared muscle/SVG module:** `app/src/lib/bodymap.jsx` exports `MUSCLES`, `SHAPES`, `EX_DB`, color constants (`PRIMARY_FILL`, `PRIMARY_HOVER`, `PRIMARY_STROKE`, heat vars), `calcMuscles`, `BodySVG`, `HeatmapBodySVG` (accepts `onHover(id|null)` and `hovered` props — when `onHover` is set the internal tooltip is suppressed), and `useIsMobile`. Do not duplicate these in component files.
- **Shared hooks:** `app/src/lib/hooks.js` — exports `useDebouncedSearch(delayMs?)` (returns `{ search, setSearch, debouncedSearch }`; debounced value is raw — trim/lowercase at use-site) and `useFetch(fn, deps?)` (returns `{ data, loading, error, setData }`; `setData` allows optimistic updates; use only for data fetched once per mount without mutation-driven refetches). Import from here; do not copy the timer pattern locally.
- **Shared utilities:** `app/src/lib/utils.js` — exports `toBase64`, `getMediaType`, `buildMuscleMapFromExercises` (with EX_DB fallback, for confirm/edit steps), `buildMuscleMapFromSession` (reads saved DB session for History read mode), `buildRecMuscleMap` (for recommendation body maps), `isInvalidNum` (validates sets/reps as integers 1–99), `callClaude(body)` (authenticated fetch to `/api/claude` — returns raw `Response`; always call `await res.json()` to read the body), `inferMusclesFromName(name)` (calls Claude Sonnet text API to infer muscle IDs for a single exercise name — returns `{ primary, secondary }` or `null`; handles markdown code fences defensively), `extractMuscles(session)` (splits `muscle_activations` into primary/secondary Sets, removes primary from secondary), `toWeekIso(date)` (Date → `"2026-W19"` ISO week string), `weekIsoToMonday(weekIso)` (`"2026-W19"` → Monday `Date`), `isoWeekMonday(date)` (Date → Monday `Date` of that ISO week, local time), `toIsoDate(date)` (Date → `"yyyy-MM-dd"` string using local time getters — replaces `date-fns` `format`), `getIntlLocale()` (maps `i18n.language` to the IETF tag `Intl` expects, e.g. `"nb" → "no"`). Do not redefine these locally in component files.
-- **Shared Claude config:** `app/src/lib/prompts.js` — exports `CLAUDE_MODEL_VISION` (sonnet-4-6, for image analysis), `CLAUDE_MODEL_TEXT` (sonnet-4-6, for recommendations), `RECS_PROMPT_VERSION` (integer — bump whenever `buildPeriodRecommendPrompt` or the model changes; old cache entries are swept by the weekly cleanup job; also keep `RECS_PROMPT_VERSION` in `app/api/recsCacheCleanup.js` in sync), `ANALYZE_PROMPT`, `buildRecommendPrompt(trained, untrained)`, `buildPeriodRecommendPrompt(periodDays, sessionCount, trainedLabels, untrainedLabels)`, `buildMuscleInferencePrompt(name)` (cheap text-only call for single-exercise muscle inference — strips `<>` from the name and wraps it in `…` XML tags per Anthropic's prompt injection boundary pattern; 3 regression tests in `prompts.test.js`). All model IDs and prompt text live here; update in one place.
+- **Shared Claude config:** `app/src/lib/prompts.js` — exports `CLAUDE_MODEL_VISION` (sonnet-4-6, for image analysis), `CLAUDE_MODEL_TEXT` (sonnet-4-6, for recommendations), `RECS_PROMPT_VERSION` (integer — bump whenever `buildPeriodRecommendPrompt` or the model changes; old cache entries are swept by the weekly cleanup job; **must also be bumped in `app/api/recsCacheCleanup.js`** — a CI test in `app/api/__tests__/recsVersion.test.js` fails if the two values drift), `ANALYZE_PROMPT`, `buildRecommendPrompt(trained, untrained)`, `buildPeriodRecommendPrompt(periodDays, sessionCount, trainedLabels, untrainedLabels)`, `buildMuscleInferencePrompt(name)` (cheap text-only call for single-exercise muscle inference — strips `<>` from the name and wraps it in `…` XML tags per Anthropic's prompt injection boundary pattern; 3 regression tests in `prompts.test.js`). All model IDs and prompt text live here; update in one place.
- Claude returns muscle IDs directly in JSON — local keyword matching (EX_DB) was abandoned because Norwegian abbreviations and whiteboard variants didn't match reliably. EX_DB is kept only as fallback for manually added exercises.
- SVG body uses `BODY_PATH` (bezier curves, viewBox `0 0 160 360`) — improved silhouette with curved shoulders, arms, waist and hips. Still simplified, not anatomically precise. `SHAPES` entries are either ellipses (`{ cx, cy, rx, ry }`) or SVG paths (`{ d }`); the render loop handles both. Key muscles with path shapes: `traps` (trapezoid with neck notch), `lats` (wing paths). `BodySVG` renders primary muscles as solid green glow, secondary as diagonal blue stripes (``).
- `useIsMobile(breakpoint=500)` — exported hook from `bodymap.jsx`. Below breakpoint: single body view with Front/Bak toggle. Above: side-by-side. Consumed via `BodyPanel` — do not use directly in page components.
diff --git a/app/api/__tests__/recsVersion.test.js b/app/api/__tests__/recsVersion.test.js
new file mode 100644
index 0000000..ece328b
--- /dev/null
+++ b/app/api/__tests__/recsVersion.test.js
@@ -0,0 +1,23 @@
+import { describe, it, expect } from 'vitest';
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+
+describe('RECS_PROMPT_VERSION sync', () => {
+ it('is identical in prompts.js and recsCacheCleanup.js', () => {
+ const promptsFile = readFileSync(
+ resolve(__dirname, '../../src/lib/prompts.js'),
+ 'utf8'
+ );
+ const cleanupFile = readFileSync(
+ resolve(__dirname, '../recsCacheCleanup.js'),
+ 'utf8'
+ );
+
+ const promptsMatch = promptsFile.match(/export const RECS_PROMPT_VERSION\s*=\s*(\d+)/);
+ const cleanupMatch = cleanupFile.match(/const RECS_PROMPT_VERSION\s*=\s*(\d+)/);
+
+ expect(promptsMatch, 'RECS_PROMPT_VERSION not found in prompts.js').toBeTruthy();
+ expect(cleanupMatch, 'RECS_PROMPT_VERSION not found in recsCacheCleanup.js').toBeTruthy();
+ expect(Number(cleanupMatch[1])).toBe(Number(promptsMatch[1]));
+ });
+});