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

361
README.md
View File

@ -1,49 +1,324 @@
# meal-suggestion skill
# Meal Suggestion — Kitchen Inventory + High-Protein Meals
Kitchen-inventory + high-protein meal suggestion skill for Hermes Agent.
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:
- Tracks a kitchen inventory (`inventory.json`).
- Emails **2 daily meal suggestions** (from stock) every day at 09:00.
- Emails a **weekly £30 shopping list** (Mondays 08:00) with a per-store
price comparison (Lidl preferred) and a plain-text fallback.
- Targets ~190200 g protein/day for a recovering burns victim (2 g/kg at
~95100 kg, age 46), avoids buying red meat, allows Amazon Prime + low-cost
condiments/flavourings/micronutrients when budget allows.
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.
## Files
- `SKILL.md` — the skill (rules, shop logic, 7-day history removal flow).
- `references/` — email template, protein sources, Eastbourne shop ratios,
meal-history logger spec, protein-target notes.
- `inventory.json`**LIVE DATA** (your current stock). Git-ignored; a
`inventory.example.json` is committed as a template.
- `meal-history.json`**LIVE DATA** (rolling 7-day meal log). Git-ignored.
Sender: `hpm6@txt3.com` (the agent's Gmail, via `smtp.gmail.com:587`).
Recipient: `jp@txt3.com`.
## Setup
1. Copy the skill into your Hermes skills dir:
```
cp -r meal-suggestion ~/.hermes/skills/
```
2. Create `inventory.json` from the example:
```
cp ~/.hermes/skills/meal-suggestion/inventory.example.json \
~/.hermes/skills/meal-suggestion/inventory.json
```
3. Ensure the email sender exists at `~/.hermes/.env`:
```
EMAIL_ADDRESS=hpm6@txt3.com
EMAIL_PASSWORD="your-app-password"
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587
```
4. Place the send helper:
`scripts/meal/send_meal_email.py` (reads creds from `~/.hermes/.env`,
supports `--subject --html --text --to`).
5. Create the two cron jobs (Hermes `cronjob` tool):
- Daily 09:00: `0 9 * * *` — load skill `meal-suggestion`, read inventory,
build 2 meals, email to `jp@txt3.com`, record into `meal-history.json`.
- Monday 08:00: `0 8 * * 1` — load skill `meal-suggestion`, build £30 Lidl
list + comparison, email to `jp@txt3.com`.
> General guidance, **not medical advice** — defer to the user's clinical /
> dietitian team; keep hydrated on a high-protein intake.
See `SKILL.md` for the full behavior spec and the removal-from-history flow.
---
> General guidance, not medical advice — defer to clinical/dietitian team.
## 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