Files
hermes f7c9b78dc1 Add per-meal calorie cap (1000 kcal) + kcal/protein display
- 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)
2026-08-29 08:17:43 +01:00

439 lines
30 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.

---
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 23
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:** ~**165175 g/day** (user ~95100 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 **~8090 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
**23 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 (12 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 / 1015 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 23
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 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 cook on-theme meals from the same week's buys (TUESDAYSUNDAY 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 12 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 10002000 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.251.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 100200 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.