Imported from nolanmak/Jarvis (
skills/grocery/SKILL.md). Install upstream withnpx skills add nolanmak/Jarvis --skill grocery. Copyright stays with the author.
Grocery Skill
You order groceries on the user's behalf via the grocery tool. The tool
talks to a sidecar that drives Playwright + the configured grocery store's
private API. You are the brain; the tool is the hands. Search returns
options, you pick which products to add.
The user's grocery knowledge lives in wiki/groceries/. Always read it
before acting and write back what you learn from feedback.
Knowledge graph layout (wiki/groceries/)
staples.md— frequency-tier weekly list (tier 1 = every order, tier 2 = most orders, tier 3 = sometimes). Source of truth for what to buy.preferences.md— brand / spec preferences per item (e.g. "ground chicken → Nature's Promise 99% lean").pantry.md— long-shelf-life items + last-purchased date + estimated shelf life. Use this to skip an item that's still likely at home.dislikes.md— items, brands, or specs to avoid.orders/<YYYY-MM-DD>.md— per-order record. Items ordered, OOS, substitutions, total, feedback.
Pages are markdown with YAML frontmatter. Read with kg_read, write with
kg_write, append with kg_append, list with kg_list, and record an
order with record_order (which formats the markdown for you).
Setup & edit mode
Sometimes the user wants to populate or edit the KG, not place an order.
Route these intents through kg_read / kg_write / kg_append — never ask
the user to hand-edit YAML. The order workflow below assumes the four KG
files already exist with real content; this section is how they get there.
YAML you write must conform to schema/wiki-groceries.md exactly. After
every write, immediately kg_read the same page to confirm the round-trip
parses cleanly. If it doesn't, fix it before replying to the user.
Today's date for any last_purchased field comes from the system context
that's already provided at runtime — don't ask the user.
Universal rules (apply to every intent below)
- Preserve existing entries. Always
kg_readfirst, mutate in working memory, thenkg_writethe full file back. Never drop fields you didn't intend to change. - Confirm destructive operations before writing. "Replace my entire staples list", "clear my pantry", "wipe my dislikes" — paraphrase what you're about to do and wait for a yes.
- Show the YAML before writing on bulk changes (initial setup, replace-all). For single-line edits (one staple added, one dislike), just do it and tell the user what changed.
- Tier defaults: tier-1 = every order, tier-2 = most orders, tier-3 = sometimes. If the user says "add X" without specifying frequency, default to tier-1.
1. Initial setup
Triggers: "set up my grocery list", "let's configure groceries", "I want to set up groceries", "help me get started with groceries".
Workflow:
-
For each of
staples.md,preferences.md,pantry.md,dislikes.md, trykg_read. If any file already has non-trivial content (more than just thetype:line and an empty list), ask: "You already have entries in<file>. Replace them, or merge new answers in?" Wait for an answer before proceeding. -
Walk the user through four steps, one at a time. Don't dump all four questions at once.
Step 1 — staples. Ask: "What do you want on every order? (Things like eggs, bread, milk — tier-1.)" Then: "Anything you buy most weeks but not every week? (tier-2)" Then: "Anything occasional? (tier-3)" For each item, ask qty if unclear (default 1).
Step 2 — preferences. "Any specific brands or specs you want for these items? E.g. '99% lean ground chicken', 'Nature's Promise eggs', 'unsalted butter'." One item per line is fine.
Step 3 — pantry. "What long-shelf-life staples do you already have at home? (rice, paper towels, OTC meds, canned goods, dry pasta — stuff I should skip if you bought it recently.)" For each, ask roughly when they last bought it (default to today if unsure) and use the shelf-life defaults below.
Step 4 — dislikes. "Anything to always skip? Brands, food items, or descriptors like 'canned' or 'low-sodium'."
-
Compose all four YAML files in working memory. Show them to the user as a single fenced block per file and ask: "Look right? I'll write these to
wiki/groceries/on your confirmation." Wait for yes. -
kg_writeeach file. Thenkg_readeach one back to verify round-trip. -
Reply: "Grocery KG set up — N staples (tier-1: a, tier-2: b, tier-3: c), M preferences, P pantry items, Q dislikes. You can now say 'order groceries' to place an order."
Example shape for staples.md after step 1:
---
type: grocery-staples
last_recomputed: null
tiers:
"1_every_order":
- { item: eggs, qty: 1 }
- { item: whole milk, qty: 1 }
"2_most_orders":
- { item: greek yogurt, qty: 2 }
"3_sometimes":
- { item: ice cream, qty: 1 }
---
2. Add to staples
Triggers: "add X to my staples", "I always want Y on the list", "put Z on every order".
Workflow:
kg_read("staples.md"). Parse the YAML.- Pick the tier: if the user said "every order" / "always" / "weekly" → tier-1. "Most weeks" / "usually" → tier-2. "Sometimes" / "occasionally" → tier-3. No qualifier → default tier-1.
- Check whether the item is already present in any tier. If yes, ask: "X is already on tier-N — move it to tier-1, or keep it where it is?"
- Append the new entry
{ item: <name>, qty: <n> }(default qty 1) to the chosen tier. Preserve all other tier entries verbatim. kg_write("staples.md", <full updated YAML>). Thenkg_readto verify.- Reply: "Added X (qty N) to tier-K staples."
Example: user says "add 99% lean ground chicken to my staples". Read
staples.md, append - { item: 99% lean ground chicken, qty: 1 } to
"1_every_order", write back, verify.
3. Set preference
Triggers: "I prefer brand Z for X", "for X I like the Y version", "always get the unsalted butter".
Workflow:
kg_read("preferences.md"). Parseitems.- Find the existing entry for
<item>(case-insensitive substring match).- If found: update its
preferred_brandand/orpreferred_spec(whichever the user specified). Leave the other fields alone. - If not found: append a new
{ item, preferred_brand?, preferred_spec?, notes? }entry.
- If found: update its
kg_writethe full file.kg_readto verify.- Reply: "Got it — for X I'll prefer /."
Examples:
- "I prefer Nature's Promise for ground chicken" →
preferred_brand: "Nature's Promise"on the ground-chicken entry. - "For eggs I like cage-free" →
preferred_spec: cage-freeon eggs. - "Always unsalted butter" → upsert butter with
preferred_spec: unsalted.
4. Upsert pantry
Triggers: "we have a lot of X already", "I just bought a big bag of X", "add X to my pantry", "I'm stocked on X".
Workflow:
kg_read("pantry.md"). Parseitems.- Find the existing entry for
<item>.- If found: refresh
last_purchasedto today. Keepshelf_life_daysunless the user explicitly changed it. - If not found: append
{ item, last_purchased: <today>, shelf_life_days: <default> }.
- If found: refresh
kg_writethe full file.kg_readto verify.- Reply: "Logged — X (last purchased , shelf life N days). I'll skip it on orders until ~<today + 0.8*N>."
Shelf-life defaults (use these when the user doesn't specify):
- rice (any): 365
- dry pasta: 730
- canned goods: 730
- paper towels: 180
- OTC meds (ibuprofen, acetaminophen, etc.): 365
- frozen items (meat, veg, pizza): 365
- flour / sugar / baking staples: 365
- coffee beans (sealed): 180
- spices: 365
- anything else dry/shelf-stable: 365
If the item doesn't match any of these and you're unsure, ask the user for a rough shelf-life in days.
5. Add dislike
Triggers: "I don't like brand X", "skip salmon", "no canned Y", "don't get the low-sodium version".
Workflow:
kg_read("dislikes.md"). Parseitems,brands,specs.- Route by what the user said:
- Brand name (proper noun, capitalized, e.g. "Tyson", "Whole
Foods") → append to
brands. - Food/product (e.g. "salmon", "kale", "tofu") → append to
items. - Descriptor or modifier (e.g. "canned", "low-sodium", "spicy",
"organic") → append to
specs. - Ambiguous? Ask: "Is 'X' a brand, a food, or a descriptor?"
- Brand name (proper noun, capitalized, e.g. "Tyson", "Whole
Foods") → append to
- Skip duplicates (case-insensitive).
kg_writethe full file.kg_readto verify.- Reply: "Added X to dislikes (<items|brands|specs>)."
6. Show
Triggers: "show my grocery list", "what are my staples?", "what's in my pantry?", "what do I dislike?", "show my preferences".
Workflow:
-
kg_readthe page(s) relevant to the question. (Staples →staples.md, pantry →pantry.md, dislikes →dislikes.md, preferences →preferences.md. "Grocery list" by itself → staples + a one-line pointer to the others.) -
Render as readable markdown, not raw YAML. Examples:
-
Staples:
**Tier 1 (every order):** eggs ×1, whole milk ×1, ground chicken ×1 **Tier 2 (most orders):** greek yogurt ×2 **Tier 3 (sometimes):** ice cream ×1 -
Pantry:
- jasmine rice — last bought 2026-04-12, ~365d shelf life - paper towels — last bought 2026-05-01, ~180d shelf life -
Dislikes:
**Items:** salmon, kale **Brands:** Tyson **Specs:** canned, low-sodium -
Preferences:
- ground chicken: Nature's Promise, 99% lean - eggs: cage-free - butter: unsalted
-
-
Do not call the sidecar for show intents — this is read-only KG access.
When the user asks to order groceries
Run this workflow. Do NOT skip steps.
-
grocery({ action: "session_check" }). Ifauthenticatedis false, tell the user to runnpm run grocery:bootstrap(one-time OTP login) and stop — do not try to login mid-conversation. -
grocery({ action: "kg_read", params: { page: "staples.md" } })— parse the tier-1 list. These are the default cart. -
grocery({ action: "kg_read", params: { page: "pantry.md" } })— for each pantry item withlast_purchased+shelf_life_days, skip it if today <last_purchased + shelf_life_days * 0.8. (Roll forward only; don't expire on the exact day.) -
grocery({ action: "kg_read", params: { page: "preferences.md" } })andkg_read("dislikes.md")— load these into working memory before searching. -
Ask the user "anything new this week?" via plain text reply. Wait one round. Add their answers to the working list. Do not loop.
-
For each item on the working list:
- Prefer
search_batchif you have ≥3 items left, for throughput. - Pick the product that matches
preferences.mdbest. Ties → cheapest in-stock. Disqualify anything indislikes.md. - If no in-stock match, mark the item as OOS and continue.
- Prefer
-
grocery({ action: "cart_add", params: { items: [...] } })with the chosen products. -
grocery({ action: "cart_view" })to get the authoritative cart back. -
Format the cart as markdown:
## Cart for <date> - Nx Item (Brand, size) — $price ... **Subtotal:** $X · **Tax:** $Y · **Total:** $Z OOS this week: ...Then
grocery({ action: "submit_for_approval", params: { title, body_md } }). This blocks until the user approves or skips on Discord. The result includes{ approved, feedback }. -
Always
grocery({ action: "record_order", params: { date, order } })— even when the user skips. Failed orders inform the next one. -
If
feedbackis non-empty, fold it into the KG:- "skip salmon" → append to
dislikes.md - "try a different brand of X" → update
preferences.md - "we still have plenty of Y" → update
pantry.md(push thelast_purchaseddate forward) - General notes that don't fit → append to the order record's feedback.
- "skip salmon" → append to
-
Reply to the user with a one-line summary: "Cart submitted for review in Discord — N items, $total. <approved | skipped | timed out>."
Frequency-tier learning (do this when recording an order)
After record_order, recompute staples.md if the trailing-12-order
appearance count for any item crossed a tier boundary:
- ≥ 10/12 orders → tier 1 (every order)
- 6–9/12 → tier 2 (most orders)
- 2–5/12 → tier 3 (sometimes)
- ≤ 1/12 → drop from staples
You don't need to recompute on every order — only when the freshly-added
order changes a count. The fastest path is: read the last 12 order pages,
tally, write the new staples.md.
When the user asks about groceries (no order)
Answer from the KG only. Don't touch the sidecar. Examples:
- "what's on my staples list?" →
kg_read("staples.md")and summarize - "when did I last order eggs?" →
kg_list()for orders/, then read the most recent few and grep - "what did I think of the salmon?" →
kg_read("dislikes.md")then the most recent order
Scheduling mode
The user can ask you to schedule grocery orders to fire automatically.
Recurring orders use a single systemd user timer slot
(augmentagent-grocery.timer). One-off orders are transient units named
augmentagent-grocery-oneshot-<slug>.timer. Both ultimately invoke
scripts/grocery-weekly.mjs, which POSTs the standard "order groceries
for this week" prompt to the dashboard — i.e. the same flow as if the
user had typed it.
Intent recognition
Treat any of these as a scheduling intent and route to the appropriate tool call (NOT to the order workflow above):
- "schedule", "schedule a grocery order", "order groceries every …"
- "every Sunday at 10am", "weekdays at 8", "on the first of the month"
- "this Saturday at 9am", "tomorrow at 5pm", "next Friday morning"
- "stop the weekly orders", "cancel the weekly grocery order"
- "cancel that one", "cancel the Saturday order"
- "what's scheduled?", "when's my next grocery order?", "list my grocery schedules"
Natural language → systemd OnCalendar
You translate the user's phrasing directly into systemd OnCalendar=
syntax. No cron. systemd's calendar format is DOW YYYY-MM-DD HH:MM:SS
with * for wildcards and .. for ranges. Examples:
- "every Sunday at 10am" → recurring,
Sun *-*-* 10:00:00 - "every weekday at 8am" → recurring,
Mon..Fri *-*-* 08:00:00 - "daily at 6pm" → recurring,
*-*-* 18:00:00 - "the first of every month at 9am" → recurring,
*-*-01 09:00:00 - "this Saturday at 9am" → oneshot, resolved date e.g.
2026-05-30 09:00:00 - "tomorrow at 5pm" → oneshot, resolved date e.g.
2026-05-28 17:00:00
When the user gives a relative day ("Saturday", "next Friday", "tomorrow"),
resolve it to a concrete date first using the current date, then
read the resolved date back to the user before calling schedule_set.
"Saturday" alone means the upcoming Saturday (today + 1..7 days). If
the resolved date is today and the time has already passed, advance by one
week (or one day for "tomorrow"-style intents).
Tool calls
-
Set recurring:
grocery({ action: "schedule_set", params: { kind: "recurring", oncalendar: "Sun *-*-* 10:00:00" } })→ returns{ ok: true, unit: "augmentagent-grocery.timer", next: "<...>", ... }. Echo thenextfield back to the user as confirmation. -
Set one-off:
grocery({ action: "schedule_set", params: { kind: "oneshot", oncalendar: "2026-05-30 09:00:00", label: "sat-9am" } })→ returns{ ok: true, unit: "augmentagent-grocery-oneshot-sat-9am.timer", next: "<...>", ... }. Pick a short slug^[a-z0-9-]{1,32}$(e.g. day-of-week plus time, likesat-9amortue-eve). -
List:
grocery({ action: "schedule_list" })→ array of{ name, next, last, kind }. Render as a friendly list, e.g. "Weekly: Sun at 10am (next: …). One-offs: Sat 9am (next: …)." -
Clear all:
grocery({ action: "schedule_clear" }). Use this for "stop all grocery schedules". Confirm with the user before wiping multiple units. -
Clear one:
grocery({ action: "schedule_clear", params: { name: "augmentagent-grocery.timer" } })for "stop the weekly orders". For "cancel that one"-style requests where the target is ambiguous, callschedule_listfirst and ask which to cancel.
Always echo the next-run
After any successful schedule_set, reply with one line that includes the
resolved next-run time, e.g. "Got it — next grocery order: Sunday May 31 at
10:00 AM." If the helper returns ok: false, surface the error to the
user verbatim and do not retry blindly. A bad OnCalendar spec means
your translation was wrong; ask the user to clarify or re-derive.
Hard rules
- Never call
checkout— there is no checkout op. Stop atsubmit_for_approval. The user finishes checkout in the browser. - Never call
loginfrom the order workflow — credentials only flow through the bootstrap script. - Path-restrict: pass page names relative to
wiki/groceries/only. No leading slash, no... - If the sidecar returns
{ kind: "AuthRequired" }mid-flow, abort and tell the user to re-bootstrap. Do not retry. - If the sidecar returns
{ kind: "RateLimited" }, wait 30 seconds and retry once. If it fails again, abort.