- SKILL.md RATIONAL PORTIONS: add ~1000 kcal/meal cap; trim starch/cheese/halloumi/lentil (not protein) if over; use scripts/calc_meal.py to check - scripts/calc_meal.py: estimate protein/kcal/fibre/satfat for a meal's items, flag over_kcal (>1000) + per-item max_per_meal breaches (unit-aware); gates daily sends - scripts/verify_portions.py: now also flags KCAL CAP violations via calc_meal - references/calories.md: per-portion kcal/macro estimate table (source of truth) - references/email-template.md + meal-history.md: show Est. kcal + Protein per card/entry - daily cron prompt: compute + record kcal/protein_g on each entry, show kcal in email, enforce 1000 kcal cap before send (pushed via cronjob update)
439 lines
30 KiB
Markdown
439 lines
30 KiB
Markdown
---
|
||
name: meal-suggestion
|
||
description: "Kitchen inventory + high-protein meal suggestions. Tracks ingredients, suggests 2 daily meals from stock, and a weekly £30 shopping list. Emails jp@txt3.com from hpm6@txt3.com. For a recovering burns victim on a high-protein diet avoiding red meat."
|
||
version: 1.0.0
|
||
author: agent
|
||
license: MIT
|
||
---
|
||
|
||
# Meal Suggestion
|
||
|
||
## Context
|
||
User **jp** is a recovering burns victim who must eat a **high-protein diet** and wants to
|
||
**avoid red meat**. This skill keeps a running kitchen inventory and produces:
|
||
|
||
- **2 daily meal suggestions** (from what's in stock) — emailed every day at 09:00.
|
||
- **A weekly shopping list** on a **£30 budget** — emailed Mondays.
|
||
|
||
- **Sender:** `hpm6@txt3.com` (the agent's Gmail, via smtp.gmail.com:587).
|
||
- **Recipient:** `jp@txt3.com`.
|
||
|
||
## Inventory
|
||
File: `/home/jp/.hermes/skills/meal-suggestion/inventory.json`
|
||
|
||
Schema:
|
||
```json
|
||
{
|
||
"updated": "YYYY-MM-DD",
|
||
"diet": { "goal": "high-protein", "avoid": ["red meat"] },
|
||
"items": [
|
||
{ "name": "Eggs", "qty": 12, "unit": "count", "category": "protein", "notes": "" }
|
||
]
|
||
}
|
||
```
|
||
- `category` is one of: `protein`, `veg`, `fruit`, `dairy`, `grain`, `tinned`,
|
||
`frozen`, `condiment`, `other`.
|
||
- Keep `qty` as a number with a `unit` (`count`, `g`, `ml`, `pack`, `can`, `tbsp`, etc.).
|
||
|
||
### Updating the inventory (you own this)
|
||
- When the user says they **used/consumed** ingredients, decrement or remove the
|
||
matching item(s) with the `patch` tool, then bump `updated` to today.
|
||
- When the user **bought** items, add or increase them.
|
||
- Do **NOT** auto-consume on a daily suggestion — the user tells you what was
|
||
actually eaten. The daily email only *proposes* meals.
|
||
|
||
## 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.
|
||
- **MEAL VARIETY (rule):** the two daily meals (lunch + dinner) should use **disjoint
|
||
ingredient sets** — avoid repeating the same items across both meals. Build each meal
|
||
from a different subset of the inventory so the day isn't the same plate twice. Only
|
||
reuse an ingredient if stock genuinely forces it, and keep any reuse to the minimum.
|
||
This often means leading one meal with a non-red-meat protein (lentils/beans/eggs/whey/
|
||
cheese) and the other with a different protein (e.g. in-stock red meat being used up,
|
||
or eggs if the first meal was egg-free). Honour this alongside the EGG LIMIT and the
|
||
STRICT inventory rule.
|
||
Do NOT inflate a single ingredient's quantity to an unrealistic amount just to avoid
|
||
overlap (e.g. 2 cans of baked beans in one meal). If a fully-disjoint meal can't reach
|
||
the protein target, close the gap with a whey shake / extra eggs (third hit) rather than
|
||
unrealistic quantities.
|
||
|
||
- **RATIONAL PORTIONS (hard rule):** every meal must be a *realistic, balanced, interesting*
|
||
plate — NOT overloaded with one ingredient. Each protein item has a `max_per_meal` cap in
|
||
`inventory.json` (expressed in that item's own unit, e.g. Eggs 4, Cumberland sausages 6,
|
||
Cheese 0.5 "small block" ≈ 60 g, Lamb mince 200 g, Lentils 150 g, Whey 50 g, Baked beans 1 can).
|
||
The daily job MUST NOT exceed a cap in any single meal. If an item has no `max_per_meal`,
|
||
fall back to `diet.rational_portions.default_caps` (keyword match) or `unit_fallback`
|
||
(count→6, g→200, can→1). `scripts/verify_portions.py` converts caps and logged amounts to
|
||
grams so a cap stored as "small block" is compared correctly against e.g. cheese logged in grams.
|
||
OVERLOAD test (what makes a meal "silly"): ONE protein at >=70% of its cap AND supplying
|
||
>=85% of that meal's protein — e.g. a bowl of 10 sausages. A balanced plate where the lead
|
||
protein is capped but other proteins + veg contribute is NOT overload (a normal 4-egg meal
|
||
passes). So spread across 3+ different proteins (plus veg, grain, dairy) so the plate is varied.
|
||
**CALORIE CAP: each meal must stay at or under ~1000 kcal** (est., excluding unlogged cooking
|
||
fat) — run `scripts/calc_meal.py` on the meal's items to check. If a capped+balanced meal
|
||
would exceed 1000 kcal, trim starch/cheese/halloumi/lentil portions (not protein) until it fits.
|
||
**If capped + balanced meals still can't reach ~165 g/day with current stock, DO NOT fake it**
|
||
— produce the best balanced meal you can, state the realistic total, and list the top 2–3
|
||
things to buy (eggs, whey, chicken, tuna) to close the gap. Never suggest "10 sausages" or any
|
||
single-ingredient overload. This rule overrides "maximize protein" — balance and realism beat a number.
|
||
- **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
|
||
suggestions so it isn't wasted. Deprioritize it behind the high-protein
|
||
non-red-meat options, but do NOT exclude it just because it's red meat.
|
||
- 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:** ~**165–175 g/day** (user ~95–100 kg × 1.7 g/kg — evidence ceiling for
|
||
trained/resistance-adapted adults; burns recovery + preserving lean mass while losing ~15 kg
|
||
fat). Aim each meal at **~80–90 g protein**
|
||
and **STATE the per-meal and daily protein totals** in the email. If 2 meals can't
|
||
reach ~165 g with current stock, say so and suggest a third hit (whey shake, extra
|
||
eggs, Greek yogurt) to close the gap.
|
||
- **Weight-loss framing:** keep meals calorie-moderate — bulk with veg, go easy on oven
|
||
chips, white bread, and oil; lead with lean protein. Fat loss comes from a mild calorie
|
||
deficit, NOT from cutting protein.
|
||
- **Note:** this is general guidance, not medical advice — defer to the user's
|
||
clinical/dietitian team and keep hydration up on a high-protein intake.
|
||
- Each meal block: **name**, the **ingredients + approx amounts** used, a short
|
||
**2–3 step method**, and the **protein total (g)**.
|
||
- **Make meals EASY & TASTY, not just high-protein.** When generating each day's
|
||
suggestions, do a quick web search (1–2 queries) for *easy, tasty, high-protein,
|
||
budget meals* using the proteins you have in stock (e.g. "easy tasty high protein
|
||
meal eggs chicken tuna lentils no cook"), and use real recipes as inspiration so the
|
||
two meals are genuinely appetising and varied day to day — not the same plate repeated.
|
||
Prefer no-cook / one-pan / 10–15 min methods. Draw on `references/food-health.md`
|
||
(tuna-lentil salad, tofu stir-fry, veggie chilli, Mexican chicken stew, chickpea
|
||
curry jacket potato, egg & spinach scramble, lentil soup, mackerel bowl, etc.).
|
||
- **Healing + health micronutrients:** fold in wound-healing nutrients (Vitamin C,
|
||
Zinc, Vitamin A, Copper, Iron+VitC pairing, Vitamin K) and 46-year-old health picks
|
||
(Vitamin D, B12, Magnesium, Omega-3) where the stock allows — see
|
||
`references/food-health.md`. Also favour foods that support **mental acuity**
|
||
(leafy greens, eggs/choline, berries, walnuts, olive oil, whole grains, hydration) and
|
||
**visual acuity** (lutein/zeaxanthin from greens + egg yolk, omega-3 DHA, Vit C/E/zinc)
|
||
— these overlap heavily with the above, so a few smart picks (oily fish, eggs with
|
||
yolk, greens, nuts/seeds) cover all three goals at once. Don't overcomplicate; note
|
||
them as gentle "health boost" lines, not medical advice.
|
||
- **STRICT inventory rule:** both daily meals MUST be built ONLY from items the user
|
||
ACTUALLY HAS in `inventory.json` at the time. Never invent, assume, or add an
|
||
ingredient that isn't listed there. Recipe/web inspiration is fine, but only use
|
||
in-stock items — substitute with an in-stock item or drop the idea. Before sending,
|
||
cross-check every ingredient against inventory.json and remove anything not present.
|
||
If stock is too sparse for a real high-protein meal, say so and list the top 2–3
|
||
things to buy — do NOT fabricate meals from items you don't have.
|
||
|
||
## Email format
|
||
Send via the helper (it reads SMTP creds from `~/.hermes/.env`):
|
||
```bash
|
||
python3 /home/jp/.hermes/scripts/meal/send_meal_email.py \
|
||
--subject "Your Meals for Today — <date>" \
|
||
--html /tmp/meal_today.html \
|
||
--to jp@txt3.com
|
||
```
|
||
HTML must be self-contained (inline CSS), mobile-friendly, dark-on-light, with a
|
||
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).
|
||
|
||
**Theme start rule (IMPORTANT):** the cuisine is picked by the **Monday weekly job**
|
||
and it themes that week's **shop basket**, but it applies to **MEALS from TUESDAY
|
||
onward**. Monday's daily meal suggestion is cooked **UNTHEMED** from whatever is
|
||
currently in stock (the shop items aren't in inventory yet). From Tuesday the daily
|
||
meal job cooks **on-theme**. If `cuisine-rotation.json` has no `current` cuisine set
|
||
(e.g. cleared between weeks, or the rest of a week with no active theme), meals are
|
||
unthemed. This keeps a fresh shop's new ingredients aligned with themed meals without
|
||
forcing a theme onto the day the shop is only proposed.
|
||
|
||
- 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 1–2 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 cook on-theme meals from the same week's buys (TUESDAY–SUNDAY only; Monday is
|
||
unthemed)** (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)
|
||
- **WEEKLY PROTEIN FLOOR (hard rule):** the non-whey protein bought in the Monday shop MUST be
|
||
enough to cover the week. Required = 170 g/day × 7 = **1190 g** of **non-whey** protein
|
||
(whey is excluded — it is a third-hit top-up, not a meal base). Estimate the non-whey protein
|
||
ALREADY in `inventory.json` (sum each protein/cheese/dairy item's qty × its per-g from
|
||
`references/protein-sources.md`). The shop's bought non-whey protein must total at least
|
||
`MAX(0, 1190 − in_stock)` g. **SPREAD it across ≥3 distinct protein sources** — no single
|
||
source may exceed ~50% of the bought total (never "20 eggs only", never one tub). If £30 can't
|
||
reach the floor, get as close as budget allows and STATE the shortfall + what to buy next.
|
||
Record `bought_protein_g_excl_whey` and `stock_protein_g_excl_whey` on `last-shop.json` and
|
||
show both in the email so weekly coverage is visible.
|
||
- 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
|
||
all health goals at once — favour overlap picks: oily fish (mackerel/salmon/tuna →
|
||
omega-3 DHA for healing + brain + eyes), eggs with yolk (choline + lutein), leafy
|
||
greens/broccoli/peas (Vit C/K/lutein), nuts/seeds (Mg/Zn/Cu + ALA), berries
|
||
(flavonoids), sweet potato/carrot (Vit A), Greek yogurt/cottage cheese/lentils/tofu
|
||
(protein). Add a short "health coverage" line in the email showing how the basket
|
||
hits healing + brain + eyes + 46yo needs.
|
||
- Email subject: `Weekly Shopping List — <date>`.
|
||
- This does **not** modify the inventory.
|
||
- **Micronutrients & vitamins (user is 46):** you MAY include 1–2 low-price
|
||
health-support items on the weekly shop when budget allows under £30, to support
|
||
overall health alongside the high-protein diet. Sensible, evidence-aware picks for a
|
||
46-year-old on high protein with limited red meat: vitamin D (low sun exposure risk),
|
||
omega-3 (fish-oil or flax/chia), magnesium (recovery/sleep), and a B12 source if
|
||
relying on eggs/dairy over meat. Prefer food-first (oily fish, leafy greens, seeds);
|
||
only add a supplement line (e.g. "Vitamin D3 1000–2000 IU", "Omega-3 caps") when
|
||
budget permits and label it clearly as a general-health suggestion, NOT medical advice.
|
||
Keep each low-cost and behind protein + veg + staples in priority.
|
||
- **Condiments & flavourings:** you MAY include a small item or two from the
|
||
spice/sauce/condiment aisle (e.g. fresh herbs, lemon, soy, spices, stock cubes,
|
||
passata, a hot sauce) so meals are more interesting — keep each low-price and only
|
||
if budget allows under £30. These are optional "interest" buys; protein + veg +
|
||
staples come first.
|
||
|
||
## Where to shop (Eastbourne) — single-shop, per-store totals
|
||
User is in **Eastbourne**. Local options: **Lidl, Sainsbury's, Co-op, Tesco Express,
|
||
and a local Londis** (good deal on milk: 2L £1.40). Memberships: Tesco Clubcard, Co-op member.
|
||
|
||
**User preference (confirmed): Lidl is the PREFERRED shop.** It's a weekly trip so the
|
||
extra distance is acceptable; Lidl gives the best value AND leaves budget for essential
|
||
micronutrients and the occasional treat. The other stores (Tesco Express, Sainsbury's,
|
||
Co-op, Londis) are shown ONLY for comparison, and as the fallback for days he doesn't
|
||
want to make the longer journey.
|
||
|
||
Grounded in Which? 2026 monthly price index (93-item basket; Lidl £160.70 baseline):
|
||
- Lidl ≈ 1.00x — **CHEAPEST, preferred** (whey, eggs, chicken, lentils, frozen veg, milk, tuna, bananas, plus fruit/veg for micronutrients + a treat)
|
||
- Tesco superstore w/ Clubcard ≈ 1.17x; **Tesco Express** adds convenience premium → ~1.22x WITH Clubcard
|
||
- Sainsbury's w/ Nectar ≈ 1.17x
|
||
- Co-op ≈ 1.30x nominal — membership = annual dividend, not instant discount
|
||
- Londis ≈ 1.25–1.35x indicative; milk cheap but full basket pricier → top-up only
|
||
|
||
**Weekly email MUST:**
|
||
1) Build ONE single-shop basket available at **Lidl** (the default recommended shop).
|
||
2) Show a **"Same basket — where else?" comparison table** (Lidl / Tesco Express Clubcard
|
||
/ Sainsbury's Nectar / Co-op member / Londis) with estimated totals + delta vs Lidl.
|
||
Label as ESTIMATES from Which? 2026 ratios + known local prices (Londis milk £1.40),
|
||
NOT live scans. Do NOT run a live web search.
|
||
3) **Recommend Lidl** as the primary shop — note it's a weekly trip so distance is fine
|
||
and it best fits the budget incl. micronutrients/treats. List the others as fallback
|
||
only ("if you'd rather not make the trip: Tesco Express is closest").
|
||
4) Single-shop the list — don't split across stores UNLESS a specific item's local price
|
||
is **definitely known** to be lower elsewhere (e.g. Londis milk 2L at £1.40 is a
|
||
confirmed cheaper line). Estimated basket ratios alone are NOT grounds to split.
|
||
5) You MAY suggest ordering certain items via **Amazon Prime** when it's clearly better
|
||
value (e.g. whey protein 1 kg is often cheaper per gram on Prime than in-store). If
|
||
used, show the Prime item separately and note it's a delivery, not a store trip.
|
||
6) Robustness: the comparison table must be clearly visible. ALSO attach a plain-text
|
||
alternative (send_meal_email.py --text) so the comparison survives even if the mail
|
||
client strips HTML tables.
|
||
|
||
## Split-store & Amazon Prime rules
|
||
- **Default: single shop at Lidl.** Do NOT split the basket across stores based only on
|
||
the estimated Which? 2026 ratios — those are approximations. Splitting is allowed ONLY
|
||
when a **specific item's local price is definitely known** to be lower elsewhere
|
||
(e.g. Londis milk 2L at £1.40 is a confirmed cheaper line, so milk could be a Londis
|
||
top-up). State the known price when you do this.
|
||
- **Amazon Prime:** you MAY suggest ordering specific items via Amazon Prime, but ONLY
|
||
when it is **genuinely better value or more convenient** — do not assume Prime is
|
||
automatically cheaper. Reality check (2026): Lidl whey is ~£9.99/500g (≈£20/kg);
|
||
Prime branded whey (Bulk/Myprotein) is typically £18–£27/kg, so it's roughly comparable
|
||
and Lidl often wins on a single trip. Suggest Prime for whey ONLY when (a) a confirmed
|
||
deal drops it below Lidl's per-kg price, or (b) the user prefers delivery over the trip.
|
||
When suggesting, show both prices side by side and state which is cheaper. Show any
|
||
Prime item as a separate "delivered via Prime" line, distinct from the in-store Lidl list.
|
||
- When neither applies, keep everything in the one Lidl list.
|
||
|
||
## 7-day meal history (for inventory removal)
|
||
A rolling log lives at `meal-history.json` (same skill dir). The **daily job UPSERTS**
|
||
its two meal entries each run (replaces any same `date`+`meal`, so re-runs don't
|
||
duplicate) and **prunes entries older than 7 days**. Each entry:
|
||
`{ date, weekday, meal: "lunch"|"dinner", name, items: [{name, qty, unit}] }`.
|
||
Only substantive food items are logged — pure condiments (oil, salt, pepper, spices,
|
||
garlic, soy, etc.) and the optional "third hit" snack are NOT logged. Units match
|
||
`inventory.json` where possible.
|
||
|
||
**On-demand removal (user says e.g. "remove Wednesday's dinner from inventory"):**
|
||
1) Resolve the weekday/date to the matching entry (weekday is relative to today; if
|
||
ambiguous, ask). 2) For each `item` in that entry, find the matching inventory entry
|
||
by name (case-insensitive). 3) Subtract `qty`; if the result is ~0 or negative,
|
||
remove the inventory entry; else update it. 4) Skip any item NOT present in inventory
|
||
(e.g. a suggested-but-never-bought item) and say so. 5) Bump `updated` to today.
|
||
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).
|
||
|
||
### Health / calorie review of the meal log (on-demand, one-off)
|
||
If the user asks to "analyse the last meals file" / "show calories for each meal" /
|
||
"how healthy is the plan", run `scripts/analyze_history.py` (optionally
|
||
`--days N` to limit the window). It reads `meal-history.json`, looks each item up in
|
||
`references/nutrition-db.json` (unit-aware: converts logged qty/unit to grams, then
|
||
scales), and prints per-meal kcal + protein, per-day totals, and a window average with a
|
||
rough micronutrient read (fibre, sat fat, vit C/A, calcium) vs RDA-ish targets. The DB is
|
||
ESTIMATE-grade — condiments/oils/cooking fat are NOT logged so real meals run ~80-120 kcal
|
||
higher per cooked meal. Present the output plainly; lead with the headline (protein on
|
||
target? sat fat high? veg variety good?) and note it is NOT medical advice. Do NOT modify
|
||
any file — this is a read-only review. If you instead hand-roll nutrition math inline, you
|
||
will likely mis-handle units (a veg logged in 'g' vs 'count'): use the script + DB, not ad
|
||
hoc arithmetic.
|
||
|
||
### Import a weekly shop into inventory (exclusions supported)
|
||
The weekly job does **NOT** auto-add the shop to inventory — `last-shop.json` is
|
||
only a record of what the Monday email proposed. To bring it in, the user says
|
||
e.g. *"add everything from Monday's shop to the inventory, except milk and
|
||
vitamin D3"*. Procedure:
|
||
1) Read `/home/jp/.hermes/skills/meal-suggestion/last-shop.json` (set by the most
|
||
recent weekly run; if missing, tell the user the shop hasn't been generated yet
|
||
and ask them to wait for Monday or to list items directly).
|
||
2) Collect the `items` list. Remove any whose normalized `name` matches a named
|
||
exclusion (case-insensitive; match against the item's `name`, not just `raw`).
|
||
3) For each remaining item, **merge into `inventory.json`**: find an existing entry
|
||
by case-insensitive name match; if found, ADD its `qty` to the existing `qty`
|
||
(keep the existing `unit` if they agree, otherwise keep the existing entry's unit
|
||
and note the discrepancy); if not found, create a new entry with the item's
|
||
`name`, `qty`, `unit`, and `category`, sorted into the list. Items whose `qty`
|
||
is `null` (flagged in `warnings`) are skipped with a note asking the user for a
|
||
quantity — never invent a number.
|
||
4) Bump `updated` to today. Do NOT modify `last-shop.json`.
|
||
5) Confirm: list what was added (name + qty + unit), what was excluded, and any
|
||
items skipped pending a quantity. If the shop would push a staple already in
|
||
stock (e.g. whey, eggs), just add to the existing quantity.
|
||
Note: this is entirely on-demand — the weekly job itself never writes inventory.
|
||
|
||
## 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.
|
||
- **Don't auto-resend after a stock update.** When the user reports actual consumption or you
|
||
correct `inventory.json` mid-day, update the inventory and STOP — do NOT volunteer a fresh
|
||
meal email. The next scheduled 09:00 cron picks up the new stock automatically. Only
|
||
regenerate + resend when the user explicitly asks (usually because they flagged a
|
||
meal-QUALITY issue, e.g. unrealistic portions — "2 cans of baked beans is silly, be
|
||
realistic"). They said plainly "I will wait for the cron job to run." Distinguish: a
|
||
consumption report = update stock, no email; a meal-quality complaint = regenerate fresh.
|
||
- **Portion probe scope + threshold (avoid false positives).** `verify_portions.py` defaults to
|
||
TODAY only — run it that way before a send. Its OVERLOAD flag requires BOTH >=70% of one
|
||
protein's cap AND >=85% of that meal's protein; a capped lead protein with real supporting
|
||
proteins (e.g. a normal 4-egg meal) must NOT trip it. If you widen the threshold later you'll
|
||
start flagging legitimate balanced plates — keep the >=85% dominance condition. `--all` is an
|
||
audit of history, not a send gate; historical over-cap rows (old halloumi quantities) are
|
||
already-sent and only fixed on explicit request.
|
||
- **Git-sync trap: the LIVE skill README may be a stub, the REPO README may be the real doc —
|
||
do NOT let `cp` clobber the better one.** When syncing the live skill dir into the repo
|
||
(`/home/jp/IdeaProjects/meal-suggestion`) before a commit, the two README.md files can
|
||
diverge: the live skill's README was found to have been reduced to a 49-line stub while the
|
||
committed repo README was a fuller 324-line version. Blind `cp live/README.md repo/README.md`
|
||
would silently destroy the richer doc. SAFE SYNC PROCEDURE: (1) `git show HEAD:README.md | wc -l`
|
||
and compare to the live README's line count; (2) if the committed version is longer, RESTORE it
|
||
with `git checkout HEAD -- README.md` and patch only the few figures that changed (e.g. protein
|
||
target), rather than overwriting with the stub; (3) copy SKILL.md / references/* / scripts/* from
|
||
live (those are the authoritative, richer copies); (4) never stage `.idea/` or `.agentbridge/`
|
||
— add them to `.gitignore`. Same caution applies to any tracked file that the live dir might
|
||
carry a degraded copy of.
|
||
|
||
## Automation
|
||
Two cron jobs (created via the `cronjob` tool), both loading this skill:
|
||
- Daily 09:00 — `0 9 * * *` — 2 meal suggestions.
|
||
- Monday 08:00 — `0 8 * * 1` — weekly £30 shopping list.
|
||
|
||
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".
|
||
- `scripts/verify_portions.py` — regression probe for RATIONAL PORTIONS. By DEFAULT checks
|
||
only TODAY's entries in `meal-history.json` (the meals the daily job just wrote, before
|
||
sending). It flags (a) any protein used above its `max_per_meal` (or fallback) cap, and
|
||
(b) single-ingredient OVERLOAD — one protein at >=70% of its cap that ALSO supplies >=85%
|
||
of the meal's protein (a "pile of one thing", e.g. 10 sausages). Caps are stored in each
|
||
item's own unit (e.g. Cheese 0.5 "small block"); the probe converts to grams so it can
|
||
compare against logged grams (e.g. 40 g cheese). Run it alongside `verify_eggs.py` BEFORE
|
||
sending a regenerated day. Pass `--all` to audit the full 7-day window (this WILL flag
|
||
ALREADY-SENT historical rows like old 100–200 g halloumi — audit only, NOT a send gate;
|
||
only fix history on explicit user request). Exits non-zero on violation.
|
||
- `scripts/analyze_history.py` — read-only health/calorie review of `meal-history.json`
|
||
(per-meal kcal+protein, per-day totals, window averages vs RDA-ish targets). Looks items up
|
||
in `references/nutrition-db.json` (unit-aware). Use for the "analyse the meals / show calories"
|
||
on-demand request. Does NOT modify files.
|
||
|
||
## References
|
||
- `references/protein-sources.md` — allowed proteins + example pairings.
|
||
- `references/email-template.md` — HTML email scaffold.
|
||
- `references/eastbourne-shops.md` — shop price table.
|
||
- `references/meal-history.md` — 7-day logger spec.
|
||
- `references/protein-targets.md` — protein math.
|
||
- `references/food-health.md` — research-grounded food & health knowledge base
|
||
(healing nutrition, wound-healing micronutrients, men's health at 46, easy & tasty
|
||
budget meals, flavour-without-cost). Read this when generating meals.
|
||
- `references/nutrition-db.json` — estimate per-item nutrition (kcal/protein/fibre/satfat/
|
||
vitC/vitA/calcium) with canonical unit + g_per_unit, consumed by `scripts/analyze_history.py`.
|
||
ESTIMATE-grade; update values if better figures become available.
|