| name | week |
| description | Plan the week's dinners against this week's grocery deals. Fetches the Wednesday flyer snapshot, ranks deals by the meals they unlock, drafts A/B/C option boards with stable slot addresses and 🔒 partial locks persisted to disk, and applies the rotation guard and preference rules. Use when the user runs /meal-planner:week, wants to plan meals for the week, or resume an in-progress option board. Detects new vs open week automatically. |
meal-planner: week
Drive one week of meal planning. Runs any day Wed→Sat; the week is keyed to the
Wednesday flyer drop, not Sunday. All work is in the current working directory
(the data home). The wrapper is ${CLAUDE_PLUGIN_ROOT}/scripts/meal_deals.py,
run via uv run.
0. Retro first (if last week is unclosed)
Before planning, run the actuals retro for last week if it is unclosed — see
skills/confirm/SKILL.md and the retro step. Skip if already closed.
1. Detect week state
uv run "${CLAUDE_PLUGIN_ROOT}/scripts/meal_deals.py" board --data-home .
First determine state with meal_deals.week_state("."):
- new — no
boards/<wednesday>.md yet. Fetch a fresh snapshot, build the
board, draft options (steps 2–4).
- open — the board exists. Read it with
meal_deals.parse_board, show the
current options with locks intact, and continue refining (step 4). Do not
redraft locked slots.
2. Snapshot + ranked deal board (new week)
Run meal_deals.snapshot(".") then meal_deals.build_board("."). If the
snapshot comes back suspect, tell the user what looks wrong (the reason) and
ask before trusting the board — do not silently plan on drifted data.
The board's needs-matching section lists Flipp strings the map didn't
resolve. Match the obvious ones to canonical ingredients and append confirmed
pairs to deals/ingredients-map.yml (KTD5) so next week they rank
automatically. Never invent a match you're unsure of — leave it unmatched.
Also check meal_deals.roster_gaps(".", today=...) — high-savings sale proteins
(e.g. a $12.99 turkey-burger BOGO) that no roster meal can use. Surface the
top one or two to the user and offer to add a matching meal to MEALS.md
(meal_deals.write_roster), so next week that deal is usable instead of wasted.
Don't add silently — confirm the meal name first.
3. Rotation guard + drafting context
Call meal_deals.rotation_guard(".", today=...). It reads the canonical
MEALS.md roster and splits it into eligible vs excluded (cooked within
the rotation window — recency comes from actuals, so a skipped meal stays
eligible). Draft from eligible; treat excluded as off unless the user asks. Load
SIDES.md (meal_deals.load_roster(".", "SIDES.md")) for sides, and config.yml
preferences (e.g. "avoid ground chicken (max 1/week)") and honor them.
Variety is the job — deals are a constraint, not the goal. The failure mode
is chasing the week's BOGO into a monotone option (all-pork week, all-chicken
week) and recycling the same dozen meals while two-thirds of the roster goes
stale. Guard against it with two signals:
- Coverage —
meal_deals.coverage(".", today=..., n=8) returns the most
neglected meals (never-cooked first, then longest-since). Seed each option
with at least one or two of these, even when they aren't on sale, so idle
roster meals resurface. meal_deals.meal_stats(".", today=...) gives per-meal
protein + weeks-since if you want the full picture.
- Protein spread — no single option should lean on one protein for 3+ of its
4 dinners (check
meal_protein / the protein field from meal_stats), and
across the week aim for a spread (e.g. one beef, one chicken, one pork/turkey,
one pasta/veg-forward). If the sale pushes everything one direction, say so and
deliberately diversify rather than offering a same-protein sweep.
- Season —
meal_stats marks each meal off_season for today's date. Don't
offer a clearly out-of-season meal (chili/soup/stew in summer, cold grill-outs
in deep winter) unless the user asks; coverage already deprioritizes them.
Read the recent almanac with meal_deals.recent_almanac(".", today=...) —
last weeks' Offered / Outcome / Dropped. Don't re-offer something confirmed
<2 weeks ago; honor Dropped notes; and if last week was heavy on one protein,
lean the other way this week.
Each option may include one optional variation slot — a genuine twist on
a liked meal the family would be excited about (Beef Tacos → Beef Nachos). Skip
it rather than bolting a sale item onto a meal that doesn't call for it; a
deal-compliance "twist" is worse than none.
Dinners come from the roster, not the flyer. Only propose meals on
MEALS.md. If a strong BOGO has no matching roster meal (e.g. turkey burgers,
ham steak), don't invent a dinner around it — either skip it or offer to add a
new meal to MEALS.md (meal_deals.write_roster), confirming with the user
first. The flyer suggests which roster meals to favor; it never defines them.
4. Draft options with the lock grammar
Propose 2–3 labeled options (A, B, C), each 3–5 dinners numbered 1..N, sides
inline in parentheses. Give each option a short title (Grill week,
Value mix (Kroger-heavy)) and put the deal rationale in each dinner line
(Pork Ribs — Kroger St. Louis Ribs (Potatoes)).
Persist every draft revision to boards/<wednesday>.md via
meal_deals.render_board(anchor, options, notes=...) — pass the option title
per option and a board-level notes line summarizing the top BOGO hits, so the
saved board carries the reasoning, not just bare dinner names. This is the
deliberate break from "drafts stay in chat": the file is how Wed→Sat
multi-device convergence works. Numbering is stable across the session; a locked
slot keeps its address and verbatim content.
After persisting the board, call meal_deals.write_almanac(".", "<wednesday>") — it captures this week's Offered options + Deals cited into
docs/knowledge/almanac/<wednesday>.md, preserving any Outcome/Dropped already
there. Re-run it after each revision. When the user rejects an idea ("not pasta
again", "too much pork"), record it with
meal_deals.record_almanac_dropped(".", "<wednesday>", note) so next week knows.
Follow the partial-lock workflow — the full grammar and worked golden scenarios
are in references/golden-scenarios.md. In short:
- Echo the lock back explicitly before generating anything.
- Regenerate only the named slots. Locked slots stay verbatim.
- Present the refined option as the same letter with an iteration note
(
Option A (v2)), not a new letter, unless the user asked for a merge.
- Show the full option after the swap with 🔒 on locked lines, and write it
back to the board file.
Confirmation is explicit only. "Looks good" / "sounds fine" are not
confirmation. When one option is fully locked, hand off to
/meal-planner:confirm to graduate it into a menu.
kw routing (optional, runtime-detected)
Standalone (steps 1–4) is the default codepath. Detect whether the
compound-knowledge kw skills are available in this session; route through them
only if present. kw runs with the data home as cwd (it writes relative paths).
- Drafting (step 4): invoke
kw-brainstorm in Pipeline Mode, seeded
with this week's committed deal digest (the ranked board + snapshot), to widen
the option ideas before you draft. Fold its suggestions into the A/B/C options.
- After drafting: invoke
kw-confidence on the drafted options to
pressure-test assumptions before the user locks anything.
If kw is absent, do none of this and proceed standalone — the flow is complete
without it. See the README's kw table for the full contract.