Resolves Woolworths NZ online grocery exceptions — out-of-stock items, missing SKUs — by searching, substituting, and writing resolved SKUs back to Iris ingredient notes.
How this skill is triggered — by the user, by Claude, or both
Slash command
/doview-image-retriever:woolies-shopperThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The narrow job of this skill: take one or more Woolworths shopping exceptions — an item that couldn't be cart-added by the deterministic bash bulk-add phase — and resolve each one by searching, picking, asking the user when needed, cart-adding, and writing the resolved SKU back to the Iris element's `Products` attribute notes for next time.
CHANGELOG.mdREADME.mdevals/evals.jsonscripts/doctor.shscripts/install.shscripts/iris-auth.shscripts/lib/iris_attr_update.shscripts/lib/system_libs.shscripts/phase2_bulk_add.shscripts/pick.pyscripts/shop.shscripts/test-writeback.shtests/fixtures/aggregate-output.mdtests/fixtures/diagram-export.jsontests/fixtures/element-cache-hit.jsontests/fixtures/element-cached-fail.jsontests/fixtures/element-no-cache.jsontests/fixtures/element-no-product-attr.jsontests/fixtures/search-all-oos.jsontests/fixtures/search-carrots.jsonThe narrow job of this skill: take one or more Woolworths shopping exceptions — an item that couldn't be cart-added by the deterministic bash bulk-add phase — and resolve each one by searching, picking, asking the user when needed, cart-adding, and writing the resolved SKU back to the Iris element's Products attribute notes for next time.
This skill is NOT the entry point for the full weekly shop. That's ./scripts/shop.sh in the same skill directory — a master bash orchestrator that runs three phases: (1) produce the shopping list — the user picks meal-plan-vs-shopping-list and photo-vs-Iris-View-GUID (four routes); a meal-plan photo is OCR'd, confirmed, and its meals matched to existing Iris recipes before iris aggregate, while a shopping-list View GUID is read from its data.markdown_source (smart_markdown, {{element:UUID:name}} provenance). The GUID route is the user's confirmation (curate / tick off owned items in Iris first); (2) pure-bash bulk-add that walks the list, processes the un-ticked items, and cart-adds those whose element has a cached SKU; (3) this skill for exception resolution. If a user types "do my Woolies shop" into a bare Claude session, your first reply should point them at shop.sh from the terminal rather than running the whole workflow inside Claude — the orchestrator is faster and cheaper.
The exception payload shop.sh hands you (in phase 3) looks like:
{
"exceptions": [
{"reason": "no_cached_sku", "name": "Carrots", "element_id": "33333333-...", "quantity": 0.7, "unit": "Kilogram", "search": "Carrot Loose"},
{"reason": "cached_sku_failed", "name": "Pork mince", "element_id": "22222222-...", "quantity": 3, "unit": "Each", "search": "Woolworths Pork Mince 500g"},
{"reason": "no_product_attr", "name": "Mystery item","element_id": "44444444-...", "quantity": 1, "unit": "Each", "search": "Mystery item"}
],
"count": 3
}
Each exception carries a search hint — the element's Preferred product type, else its Products type, else its name — which is the best string to search Woolworths with. The reason codes are:
no_cached_sku — the element has a Products attribute but its notes hold no woolies:NNN. The common first-run case. Search, pick, cart-add, then cache the SKU (below).cached_sku_failed — a cached SKU existed but Woolworths rejected it (out of stock / discontinued). Find a replacement, cart-add, and overwrite the cached SKU.no_product_attr — the element has no Products attribute at all. Resolve the SKU, then add a Products attribute (with the SKU in its notes) via iris update element.element_fetch_failed — iris elements get failed for that element_id. Surface to the user; usually transient.no_provenance — the line carried no element id (e.g. a shopping-list-from-photo route). You can still search + cart-add, but there's no element to cache the SKU back to — tell the user, and recommend a meal-plan or shopping-list View GUID route for the cacheable fast path.This skill runs in Claude Code when invoked by shop.sh phase 3 (the master script spawns an interactive claude "…" session — not claude -p, because the skill needs a TTY to ask you about ambiguous/out-of-stock picks). It can also run in Claude Cowork if the user invokes it manually for a one-off SKU lookup. Claude Desktop (chat-only) is not supported — the skill shells out to the local iris and woolies CLIs.
Run ./scripts/doctor.sh first if you haven't been invoked by shop.sh (which has already done its own preflight). The doctor emits one JSON line:
{"ok": true, ...} — continue.{"reason": "not_installed", ...} — instruct the user to run ./scripts/install.sh.{"reason": "not_logged_in", ...} — instruct the user to run woolies login (interactive, ~25 s).{"reason": "doctor_reported_problem", ...} — surface the hint verbatim.The iris CLI just needs to be pointed at the right backend — no login required. The shopping-list diagram and ingredient elements are readable anonymously, so the core shop works with the CLI unauthenticated; it only needs the correct url.
The trap is the default: with no --url flag, no IRIS_URL env, and no url in ~/.config/iris/config.toml, the CLI falls through to http://localhost:8000, which has none of the user's data (so every lookup 404s). shop.sh's preflight handles this automatically — if the CLI would resolve to that localhost default, it exports IRIS_URL=$DEFAULT_IRIS_URL (default https://iris-api-gtb3.onrender.com — the iris-api host, not the SvelteKit frontend iris-uat.chrisbarlow.nz, whose /api path serves HTML). Override by exporting IRIS_URL or setting url in config.toml.
Auth is optional and only affects the writeback step. Anonymous sessions can read everything but cannot write, so the SKU cache writeback (refreshing confirmed: dates / persisting new SKUs) is silently skipped (iris_attr_update … || true). To enable writeback, put a token in IRIS_TOKEN (it overrides any token in ~/.config/iris/config.toml). This deployment runs in Supabase mode, where auth is awkward: password iris login is disabled (/api/auth/login → 404), and the PAT path currently 500s server-side (a backend bug — it should 401 on a bad token, not crash). The working path is a Supabase session JWT: run source scripts/iris-auth.sh (password once, then refresh-token reuse) to fetch one and export IRIS_TOKEN. Verify a writeback end-to-end with ./scripts/test-writeback.sh <element_id> (snapshots → writes a test SKU → verifies → restores).
For each entry in the exceptions payload:
Use the exception's search hint (the Preferred product type → Products type → name), not just the bare name — it's usually the exact product:
woolies search "<exception.search>" --json --limit 5
Pipe the search output to scripts/pick.py:
woolies search "<exception.search>" --json --limit 5 | \
python3 scripts/pick.py --want '{"qty": <quantity>, "unit": "<unit>", "size_hint": "...", "brand_hint": "..."}'
The picker returns one of four shapes (unchanged from v0.1.0):
{"sku": ..., "name": ..., "size": ..., "quantity": ..., "unit": ...} — unambiguous winner. Use it.{"ambiguous": true, "candidates": [...]} — ask the user via AskUserQuestion, present the top candidates with size + price.{"out_of_stock": true, "candidates": [...]} — offer the user the top 2 OOS alternatives as substitutes, or skip the line.{"no_matches": true} — surface to the user, skip.woolies cart add <sku> <quantity> --unit <Each|Kilogram>
After a successful cart-add, write the SKU back so the next shop hits the cache. The SKU is a property of the individual product, so the cache lives in the element's Products attribute notes. The Preferred product attribute is a name-only pointer (its type names the chosen product, used for the search hint) and must not carry the SKU. The shared helper in scripts/lib/iris_attr_update.sh does the get-merge-put dance against the iris CLI:
source scripts/lib/iris_attr_update.sh
NEW_NOTES="woolies:${sku} | confirmed:$(date +%Y-%m-%d)"
# Write to the first "Products" attribute row (index 0).
iris_attr_update "$element_id" "Products" 0 "$NEW_NOTES"
The reasons require slightly different handling:
no_cached_sku — a Products attribute exists with empty notes. Resolve the SKU and write it to that row (index 0). Steady-state first-run case.cached_sku_failed — the cached SKU was rejected (OOS / discontinued). Resolve a replacement and overwrite the Products notes with the new SKU (the old one is dead).no_product_attr — the element has no Products attribute. Add one carrying the resolved SKU: iris elements get the element, append {"name":"Products","type":"<product name>","notes":"woolies:NNN | confirmed:DATE","scope":"Public","lower_bound":"","upper_bound":""} to data.attributes, and iris update element <id> --data-json <data>.element_fetch_failed — surface to the user; you can still cart-add but can't write back until the element is reachable.When invoked by shop.sh, write each resolved exception (sku, name, quantity, unit, action taken — added / substituted / skipped) to $STATE_DIR/phase3-result.json so the master script can roll the summary up.
After all exceptions are resolved, run woolies cart list --json and render a compact table for the user showing the full final cart. End with: "Open https://www.woolworths.co.nz in your browser to review and submit the order." The skill never attempts checkout — that's a human-eyeballs-then-clicks-Submit step.
woolies login downloads it on first run (~30–60 s).woolies login again. Cookies normally persist for weeks.WOOLIES_PROXY=http://... env var to route through a proxy.tests/test_pick.py remains the behavioural spec for the picker (10 cases, unchanged).tests/test_phase2.sh covers the bash phase 2 against the smart_markdown format: cache hit, cached_sku_failed, no_cached_sku, no_product_attr, ticked-line skip, and quantity parsing (2 {{el}} → 2 Each, 700 g → 0.7 Kilogram). Fixtures live in tests/fixtures/ (diagram-export.json + element-*.json); the mock CLIs are in tests/mock-bin/. Add new cases there in red→green order.x N, N cans, N whole, N g); the human reviews the trolley at woolworths.co.nz, so a mis-parsed qty is caught there, not silently shipped.woolies:NNN | paknsave:MMM | confirmed:YYYY-MM-DD would be the natural extension.npx claudepluginhub cgbarlow/skills --plugin woolies-shopperQueries live Woolworths NZ product prices, specials, categories, and SKU details via CLI. Read-only, no account or browser automation needed.
Helps users find products, compare options, and manage their shopping cart on Oda via an MCP server with ID-based API.
Automates Instacart operations via Rube MCP and Composio's Instacart toolkit. Always searches for current tool schemas before execution.