Sync: EGG LIMIT hard rule + weekly cuisine rotation + scripts

- SKILL.md: add egg-limit hard rule (max 4/meal, one egg-meal/day), cuisine-rotation section
- scripts/pick_cuisine.py: seeded weekly world-cuisine picker (stable per week, no recent repeats)
- scripts/verify_eggs.py: regression guard for the egg cap
- README.md: full project documentation
- .gitignore: exclude cuisine-rotation.json + python caches (live data stays local)
This commit is contained in:
meal-agent
2026-08-27 19:07:53 +01:00
parent 9e448760cd
commit d8aa87921e
5 changed files with 614 additions and 43 deletions

View File

@ -45,6 +45,7 @@ Schema:
## Meal suggestion rules
- Suggest **two meals** per day (e.g. lunch + dinner) using only items currently in the inventory.
- **Maximize protein** per meal; lead with a protein source.
- **EGG LIMIT (hard rule):** Eggs are capped at **4 per meal** and **only ONE of the two daily meals may contain eggs** — the other meal MUST be egg-free. Never suggest more than 4 eggs in a single meal, and never put eggs in both meals. If a meal needs more protein than 4 eggs supply, close the gap with other proteins (lentils, beans, halloumi, cheese, tinned fish, whey) or a whey shake — do NOT add a 5th+ egg. This overrides the "lead with eggs" preference below.
- **Avoid red meat for PURCHASES** (beef, lamb, pork, venison, bacon, gammon) — the
weekly shopping list must NEVER suggest red meat to buy.
- **If red meat is already in the inventory, it stays usable** in daily meal
@ -53,6 +54,7 @@ Schema:
- Preferred proteins (lead with these): eggs, chicken, turkey, fish, prawns, tofu,
tempeh, lentils, beans, chickpeas, Greek yogurt, cottage cheese, quark, skim milk,
whey, tinned tuna/salmon, edamame, halloumi.
(Eggs are subject to the EGG LIMIT hard rule above: max 4 per meal, only one meal/day may include them.)
- **Protein target:** ~**190200 g/day** (user ~95100 kg × 2 g/kg — burns recovery +
preserving lean mass while losing ~15 kg fat). Aim each meal at **~90100 g protein**
and **STATE the per-meal and daily protein totals** in the email. If 2 meals can't
@ -103,8 +105,35 @@ header, the two meal cards, and a footer note
("Reply to tell me what you used and I'll update the inventory").
Use the scaffold in `references/email-template.md`.
## Weekly cuisine rotation
To keep shops and meals varied over time, each week's list is themed around a
**randomly chosen world cuisine**. This is driven by
`scripts/pick_cuisine.py` (same skill dir).
- Run `python3 scripts/pick_cuisine.py` at the **start of the weekly job**. It
picks a cuisine seeded by the week's Monday, so it is **stable within a week**
(re-runs/retries never flip the cuisine mid-week) but **varies week to week**
and avoids the last 4 cuisines (no back-to-back repeats). It prints JSON with
`cuisine` and `theme_items`, and persists the choice to `cuisine-rotation.json`.
- The chosen cuisine must **theme the entire weekly basket**: lead the protein,
veg, and 12 condiment/flavour buys with that cuisine's `theme_items`, so the
shop introduces varied ingredients. Cuisines are all Lidl-achievable on ~£30,
high-protein, and **red-meat-free by design**.
- Do NOT abandon the core rules for the theme: still hit the protein target,
still cover healing/brain/eyes/46yo micronutrients where possible, still
default to Lidl, still never buy red meat, still keep the £30 budget. Theme
items that are just flavour (spices, sauces, lime, herbs) are the optional
"interest" buys — keep them low-price and behind protein + veg + staples.
- Persist the active cuisine so the **daily meal job can read `cuisine-rotation.json`
and on-theme meals from the same week's buys** (e.g. build a Korean bowl, a
Mexican bowl, etc., from stock). The daily job should still respect the STRICT
inventory rule — only cook with what's actually in inventory.json.
- The rotation file keeps a short history; no manual tracking needed.
## Weekly shopping list (Mondays)
- Budget **£30** total. Use realistic UK supermarket prices (Tesco/Asda/Sainsbury's).
- **Pick this week's cuisine first** (see "Weekly cuisine rotation" above) and
theme the basket around it — varied ingredients keep meals interesting.
- Prioritize protein, then veg/fruit, then staples. Show item, est. price, and a
running total that lands in range.
- **APPLY the research** (`references/food-health.md`): build the basket so it covers
@ -201,11 +230,54 @@ garlic, soy, etc.) and the optional "third hit" snack are NOT logged. Units matc
6) Confirm what was removed. The user can also say "remove all of <day>" to drop both
meals, or name a specific item to override.
**Item-level overrides & partial consumption (apply BEFORE step 3 above):**
- If the user corrects a quantity for one item (e.g. "only used 100g halloumi, not
200g"), use their stated qty instead of the logged qty for that item.
- If the user says they only used PART of an item and the rest remains (e.g. "still
got half a lettuce", "there's half a head of broccoli left"), do NOT remove the whole
inventory entry — keep/restore the remaining portion (re-add at e.g. 0.5 count, or
leave prior-qty minus what was used). Never zero out an item the user says they still
have. Log the restored amount back into `inventory.json` and re-sort.
**Reconcile other meal-history entries AFTER removal (prevents stale future plans):**
The daily job only writes TODAY's two meals at 09:00 — it never pre-plans future days.
But `meal-history.json` may contain future-dated entries (pre-seeded, or written by a
re-run). After any removal that deletes an inventory item (hits ~0), scan EVERY entry
in `meal-history.json` — including future-dated ones inside the 7-day window — for
`items` that reference the now-absent inventory name. For each such entry:
- Flag it to the user: "<Day> <meal> ('<name>') still lists <gone item> but it's no
longer in inventory."
- Offer to rewrite that entry using ONLY in-stock items (substitute the missing item
for an in-stock alternative, or drop it). Do NOT silently leave a plan that can't be
cooked.
This is the only guard against "Thursday's plan used chicken, but I ate the chicken
today" — the design is correct; the stale future row is the anomaly, so fix the row,
not the design. See `references/reconcile-history.md` for the scan snippet.
## On-demand (chat)
If the user lists ingredients in chat, add them to the inventory and optionally
give an immediate 2-meal suggestion. If they ask "what should I eat today?", read
the inventory and suggest now (and offer to email it).
## Pitfalls (learned the hard way)
- **"Regenerate / resend today's meals" means GENERATE FRESH, not replay.** When the
user asks to regenerate a day's meals or re-send the email, do NOT pull the two meals
back out of `meal-history.json` and re-format them. Re-read `inventory.json`, build
two new meals from current stock (honouring the egg limit, STRICT inventory rule, etc.),
then update today's `meal-history.json` entries to match what you actually send.
The history is a log; it is not the source of truth for a regeneration request.
- **Editing a cron job's prompt: edit jobs.json AND push via the `cronjob update`
action.** The scheduler may hold jobs in memory, so a bare file edit is not enough —
the running daemon won't see it. After writing the new prompt string, call
`cronjob update job_id=<id> prompt="..."` so the in-memory job reloads. Verify with
`cronjob list` that the preview reflects the change. (Pushing to a non-running daemon
is harmless; not pushing is the silent-failure trap.)
- **The egg rule is a HARD cap, not a preference.** The daily job leaned on eggs as the
"preferred protein" and produced 8 eggs across two meals (6 + 4). Cured by (a) the
EGG LIMIT prose rule above, (b) `diet.egg_rules` in inventory.json, and (c) running
`scripts/verify_eggs.py` after any history/inventory change or before a resend.
Always run that probe before sending a regenerated day.
## Automation
Two cron jobs (created via the `cronjob` tool), both loading this skill:
- Daily 09:00 — `0 9 * * *` — 2 meal suggestions.
@ -214,6 +286,14 @@ Two cron jobs (created via the `cronjob` tool), both loading this skill:
Delivery is `local` because the email itself is the deliverable; check
`cronjob action=list` / logs if a send ever seems missing.
## Scripts (in this skill dir)
- `scripts/pick_cuisine.py` — picks the week's random world cuisine (stable per week,
varies weekly, no recent repeats); persists to `cuisine-rotation.json`. Used by the
Monday weekly job (see "Weekly cuisine rotation").
- `scripts/verify_eggs.py` — regression probe for the EGG LIMIT. Run after any change to
`meal-history.json`, `inventory.json`, or the daily job, and before sending a
regenerated day's email. Exits non-zero on violation. See "Pitfalls".
## References
- `references/protein-sources.md` — allowed proteins + example pairings.
- `references/email-template.md` — HTML email scaffold.