Files
meal-agent d8aa87921e 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)
2026-08-27 19:07:53 +01:00

325 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Meal Suggestion — Kitchen Inventory + High-Protein Meals
An automated, research-grounded meal system for a single user (jp): a recovering
burns victim on a **high-protein diet (~190200 g/day)** who is also losing fat,
and who wants to **avoid buying red meat**. It does two things on a schedule:
1. **Daily meal suggestions** — two meals (lunch + dinner) built only from what
is actually in the kitchen inventory, emailed every day at 09:00.
2. **Weekly shopping list** — a £30 Lidl basket, themed around a randomly chosen
world cuisine that week, emailed Mondays at 08:00.
Sender: `hpm6@txt3.com` (the agent's Gmail, via `smtp.gmail.com:587`).
Recipient: `jp@txt3.com`.
> General guidance, **not medical advice** — defer to the user's clinical /
> dietitian team; keep hydrated on a high-protein intake.
---
## Table of contents
- [How it works](#how-it-works)
- [Repository layout](#repository-layout)
- [Core rules](#core-rules)
- [Egg limit (hard rule)](#egg-limit-hard-rule)
- [Weekly cuisine rotation](#weekly-cuisine-rotation)
- [Inventory & meal history](#inventory--meal-history)
- [Email format](#email-format)
- [Automation (cron)](#automation-cron)
- [Scripts](#scripts)
- [Where to shop](#where-to-shop-eastbourne)
- [On-demand usage](#on-demand-usage)
- [Testing & regression guards](#testing--regression-guards)
- [Git & sync](#git--sync)
---
## How it works
The system is driven by a **skill** (`SKILL.md`) plus two cron jobs that load it.
On each run the agent:
- Reads the kitchen **`inventory.json`** (current stock).
- **Daily:** searches the web for easy, tasty, high-protein budget recipes for
inspiration, then builds two meals **strictly from in-stock items**, records
them to `meal-history.json`, and emails them.
- **Weekly:** picks a cuisine, builds a £30 Lidl basket themed around it, adds a
per-store price comparison and a health-coverage note, and emails it.
All generated meals obey the **strict inventory rule**: no ingredient may appear
that is not already in `inventory.json`. Recipe ideas are only used as
inspiration; if a recipe needs something not in stock, it is substituted with an
in-stock item or dropped.
---
## Repository layout
```
meal-suggestion/
├── SKILL.md # The operative spec: rules, format, automation
├── README.md # This document
├── .gitignore # Excludes live data (inventory/meal-history/cuisine-rotation)
├── inventory.example.json # Schema example for inventory.json
├── meal-history.example.json # Schema example for meal-history.json
├── references/ # Knowledge base the agent reads when generating
│ ├── email-template.md # HTML email scaffold (inline CSS, mobile-friendly)
│ ├── food-health.md # Healing + 46yo health + easy/tasty meal research
│ ├── protein-sources.md # Allowed proteins + example pairings
│ ├── protein-targets.md # Protein math (2 g/kg)
│ ├── meal-history.md # 7-day logger spec + reconcile-from-inventory logic
│ └── eastbourne-shops.md # Shop price table (Lidl preferred)
├── scripts/
│ ├── pick_cuisine.py # Picks this week's world cuisine (seeded, varied)
│ └── verify_eggs.py # Regression guard for the EGG LIMIT hard rule
└── (live, git-ignored)
├── inventory.json # CURRENT stock — drifts as the user eats/buys
├── meal-history.json # 7-day rolling log of suggested meals
└── cuisine-rotation.json # Current + recent weekly cuisine picks
```
The live skill directory is `/home/jp/.hermes/skills/meal-suggestion/`; this repo
is a synced mirror under `~/IdeaProjects/meal-suggestion/`. The two stay in step
(see [Git & sync](#git--sync)).
---
## Core rules
- **High-protein target:** ~190200 g/day (≈2 g/kg at ~95100 kg, for burns
recovery + lean-mass preservation during ~15 kg fat loss). Aim ~90100 g per
meal and **state the per-meal and daily protein totals** in the email.
- **Weight-loss framing:** mild calorie deficit via bulking with veg and going
easy on chips/bread/oil — **not** by cutting protein.
- **Red meat:** already in stock is *usable* (use it up, deprioritised), but the
shopping list must **never suggest buying red meat** (beef, lamb, pork,
venison, bacon, gammon).
- **Strict inventory rule:** both daily meals are built ONLY from items in
`inventory.json`. Nothing invented, assumed, or added.
- **Tasty & varied:** the agent does a 12 query web search for easy, high-protein
budget meals and draws on real recipes so meals aren't the same plate repeated.
Prefers no-cook / one-pan / 1015 min methods.
- **Health coverage:** meals/items favour wound-healing nutrients (Vit C, Zinc,
Vit A, Copper, Vit K, iron+VitC) and 46-year-old health (Vit D, B12,
Magnesium, Omega-3) plus mental + visual acuity (leafy greens, eggs/choline,
berries, walnuts, olive oil, lutein/zeaxanthin, omega-3 DHA). Noted as gentle
"health boost" lines, not medical advice.
---
## Egg limit (hard rule)
Eggs were previously overloaded (a day suggested 8 eggs across two egg-bearing
meals). This is now a **hard cap**, enforced three ways:
1. **In SKILL.md:** *max 4 eggs per meal; only ONE of the two daily meals may
contain eggs — the other must be egg-free.* If a meal needs more protein than
4 eggs supply, close the gap with lentils/beans/halloumi/cheese/tinned fish/
whey — never a 5th+ egg. This overrides the "lead with eggs" preference.
2. **In inventory.json:** `diet.egg_rules` carries `max_per_meal: 4` and
`max_meals_per_day_with_eggs: 1`; the Eggs item note states the cap. Every run
reads the limit from the data itself.
3. **In scripts/verify_eggs.py:** a regression guard that checks the persisted
history against `diet.egg_rules` and exits non-zero on violation. Run it after
any change to inventory/meal-history or the daily job, and before sending a
regenerated day's email.
---
## Weekly cuisine rotation
To keep shops and meals varied over time, each week's list is themed around a
**randomly chosen world cuisine**, driven by `scripts/pick_cuisine.py`.
- **Stable within a week:** the pick is seeded by the week's Monday ISO date, so
re-runs / retries of the Monday job never flip the cuisine mid-week.
- **Varied over time:** the last 4 chosen cuisines are excluded from the pool
(no back-to-back repeats).
- **Persisted:** writes the choice to `cuisine-rotation.json`
(`week_monday`, `cuisine`, `theme_items`) so the **daily job can read it** and
cook on-theme meals from the same week's buys.
- **Cuisine pool (all Lidl-achievable on ~£30, high-protein, red-meat-free by
design):** Mexican/Tex-Mex, Greek/Mediterranean, Indian/South Asian, Thai, East
Asian/Japanese, Middle Eastern/Levantine, Italian, Korean.
- **Themes without breaking rules:** the cuisine leads protein/veg/condiment buys
with its `theme_items`, but still respects £30 budget, protein-first, healing/
46yo coverage, Lidl-default, and never-buy-red-meat. Pure flavour buys
(spices/sauces/lime/herbs) are the optional low-price "interest" items.
The weekly email subject/header shows `Cuisine: <name>`.
---
## Inventory & meal history
**`inventory.json`** — current stock. Schema:
```json
{
"updated": "YYYY-MM-DD",
"diet": {
"goal": "high-protein",
"avoid_purchase": ["red meat"],
"egg_rules": { "max_per_meal": 4, "max_meals_per_day_with_eggs": 1,
"note": "..." },
"age_years": 46, "bodyweight_kg": "95-100",
"protein_per_kg": 2, "protein_target_g_per_day": "190-200",
"weight_loss_goal_kg": 15
},
"items": [
{ "name": "Eggs", "qty": 10, "unit": "count", "category": "protein",
"notes": "Hard cap: max 4 eggs per meal; only ONE of the two daily meals may use eggs." }
]
}
```
- `category``protein, veg, fruit, dairy, grain, tinned, frozen, condiment, other`.
- **You own the inventory.** When the user says they *used/consumed* ingredients,
decrement/remove them (via the `patch` tool) and bump `updated`. When they
*bought* items, add/increase them. The daily email only *proposes* meals — it
does **not** auto-consume.
**`meal-history.json`** — rolling 7-day log. The daily job **UPSERTS** today's two
entries (replaces same `date`+`meal`, so re-runs don't duplicate) and **prunes**
entries older than 7 days. Each entry:
```json
{ "date": "YYYY-MM-DD", "weekday": "Thursday",
"meal": "lunch", "name": "Halloumi & Red Lentil Power Bowl (egg-free)",
"items": [{ "name": "Halloumi", "qty": 100, "unit": "g" }] }
```
Only substantive food items are logged — pure condiments and the optional
"third hit" snack are not. This log is what the on-demand inventory-removal
feature reconciles against.
---
## Email format
Sent via `python3 /home/jp/.hermes/scripts/meal/send_meal_email.py` (reads SMTP
creds from `~/.hermes/.env`). Both HTML (inline CSS, mobile-friendly, dark-on-light)
and a plain-text alternative are produced so the plan survives HTML-stripping
clients.
- **Daily:** header (date + `high-protein · ~190200g/day · no red meat bought`),
two meal cards (name, ingredients, 23 step method, protein total, short
"health boost" note), footer (`Reply to tell me what you used and I'll update
the inventory`).
- **Weekly:** shopping table (item, est. price, running total ≤ £30, Total row),
header with `Cuisine: <name>`, a "health coverage" note (healing + brain + eyes
+ 46yo + cuisine theme), a "Same basket — where else?" per-store comparison
table, and a "Same basket — where else?" plain-text mirror.
---
## Automation (cron)
Two cron jobs (created via the `cronjob` tool), both loading the
`meal-suggestion` skill:
| Job | Schedule | Purpose |
|-----|----------|---------|
| Daily meal suggestions | `0 9 * * *` | 2 in-stock meal suggestions → email |
| Weekly shopping list | `0 8 * * 1` | £30 cuisine-themed Lidl basket → email |
Delivery is `local` (the email is the deliverable); check `cronjob action=list` /
logs if a send seems missing. The weekly job runs `pick_cuisine.py` **first**,
then themes the basket; the daily job reads `cuisine-rotation.json` and prefers
on-theme cooking while still obeying the strict inventory rule and the egg cap.
---
## Scripts
### `scripts/pick_cuisine.py`
Picks this week's cuisine.
- `python3 scripts/pick_cuisine.py` — pick (or reuse this week's) and persist.
- `python3 scripts/pick_cuisine.py --dry-run` — print only, do not write.
- Prints JSON: `{"week_monday", "cuisine", "theme_items", "reused"}`.
- Seeded by the week's Monday → stable within a week; excludes the last 4
cuisines → no back-to-back repeats.
### `scripts/verify_eggs.py`
Regression guard for the EGG LIMIT contract. Reads `inventory.json`
(`diet.egg_rules`) and `meal-history.json`, checks no meal exceeds
`max_per_meal` and no day exceeds `max_meals_per_day_with_eggs`. Exits non-zero
on violation. Run after any change to the egg logic or before sending a
regenerated day.
---
## Where to shop (Eastbourne)
User preference (confirmed): **Lidl is the preferred shop** — a weekly trip, so
the extra distance is fine; best value and leaves budget for essential
micronutrients and the occasional treat. Others (Tesco Express, Sainsbury's,
Co-op, Londis) are comparison/fallback only.
Grounded in Which? 2026 (93-item basket; Lidl ≈ £160.70 baseline):
- Lidl ≈ 1.00x — cheapest, preferred
- Tesco superstore w/ Clubcard ≈ 1.17x; Tesco Express ≈ 1.22x
- Sainsbury's w/ Nectar ≈ 1.17x
- Co-op ≈ 1.30x (membership = annual dividend)
- Londis ≈ 1.251.35x (milk 2L £1.40 is a confirmed cheaper line)
The weekly email shows a "Same basket — where else?" comparison (Lidl / Tesco
Express Clubcard / Sainsbury's Nectar / Co-op member / Londis) as **estimates**
from the Which? 2026 ratios + known local prices — no live web search. Default is
a single Lidl shop; split only when a specific item's local price is *definitely*
known lower.
---
## On-demand usage
- **"What should I eat today?"** — read `inventory.json`, suggest now, offer to email.
- **"Remove Wednesday's dinner from inventory"** — resolve the entry, decrement
each logged item from `inventory.json` (remove if ~0), skip items not present,
bump `updated`, confirm. Works per-item and partial-consumption overrides too.
- **Adding stock** — list items in chat; they're added to `inventory.json` and an
immediate 2-meal suggestion can follow.
---
## Testing & regression guards
- `python3 scripts/verify_eggs.py` — asserts the EGG LIMIT holds across
`meal-history.json` using the persisted `diet.egg_rules`. Should print
`PASS: egg limit OK (max 4/meal, max 1 egg-meal/day)`.
- The cuisine picker is deterministic per week (Monday-seeded) and was verified to
produce no consecutive repeats across 8 simulated weeks.
---
## Git & sync
This repo (`~/IdeaProjects/meal-suggestion/`) is the version-controlled mirror of
the live skill (`/home/jp/.hermes/skills/meal-suggestion/`).
- **Live data is git-ignored:** `inventory.json`, `meal-history.json`,
`cuisine-rotation.json`, and `__pycache__/`. Only the spec, references,
examples, and scripts are committed.
- **Sync workflow:** copy the changed source files (currently `SKILL.md` and
`scripts/`) from the live skill dir into this repo, then commit. Keep
`.gitignore` excluding live data in both places.
```bash
# from /home/jp/.hermes/skills/meal-suggestion
cp SKILL.md /home/jp/IdeaProjects/meal-suggestion/SKILL.md
cp scripts/pick_cuisine.py scripts/verify_eggs.py \
/home/jp/IdeaProjects/meal-suggestion/scripts/
# commit the mirror
cd /home/jp/IdeaProjects/meal-suggestion
git add -A && git commit -m "Sync: egg-limit hard rule + weekly cuisine rotation"
git push
```
Version history of the mirror:
- `72ce56e` Initial meal-suggestion skill (logic + template; live data git-ignored)
- `c5f4fb1` Sync: food-health research, mackerel, easy/tasty + acuity, weekly health-coverage
- `9e44876` Sync: strict inventory-only rule for daily meals
- *(current)* Sync: EGG LIMIT hard rule + weekly cuisine rotation + scripts