Case study · Community Food Co-op · Jun–Nov 2026

A bakery ordering system, designed and shipped by one designer directing AI agents

Community Food Co-op’s custom cake orders arrived in a shared inbox and were copied by hand onto carbon tickets. I replaced that with a WooCommerce plugin: a pickup-first cake builder, staff dashboards, printed tickets, emails and a Thanksgiving deli layer, live since 1 November 2026.

Days

142

From first commit to a live store.

git log · STATUS.md:161

Commits

1,503

1,482 of them with a Claude co-author trailer.

git log --all · 2026-11-07

Assertions

6,089

In a full test run, 1 known red.

tests/cli/README.md · 2026-11-05

Tickets

69

61 closed, median 9 days to close.

gh issue list · 2026-11-07
Bakery ordering landing page at desktop width: hero copy on the left, three seasonal specials, and category cards with photos.
The bakery landing page on a phone: header, seasonal specials and a list of bakery categories with photos.

DesktopThe landing page at 1440 px, from the desktop pass.

PhoneThe same landing at 375 px, the width every screen was designed at first.

01 · The problem

Orders arrived as email and were copied onto carbon tickets by hand

The co-op sold custom cakes through Catapult/WebCart, its POS vendor’s storefront. Each order landed in a shared inbox and was re-keyed onto a three-colour carbon ticket: white for the store, yellow for the deli, pink for the bakery. Cancellations were phone calls.

In October the deli joined the project. Until then the deli lead had run Thanksgiving pre-orders alone: every order arrived as an email that couldn’t be searched, and each one was typed three times, into a season master, a pickup-day sheet and the store’s guide.

The deli’s 25-sheet workbook showed why the numbers drifted. No formula on any sheet referenced another, so anything that appeared twice had been typed twice. The 2025 season had 42 orders; as the spec put it, “its cost is the retyping, not the volume.”

“The amount of hours that I put in, putting this in here, is ridiculous.”

— The deli lead, discovery interview, October 2026 · docs/deli/conversation-digest.md:162-178

Four groups use the system:

  • Customersordering a cake on a phone.
  • Bakery staffwho print and progress orders, and enter phone orders through the same storefront.
  • Deli staffat two stores, from the deli lead to the service desk, each with their own view and print button.
  • Marketingwho own descriptions and photos; the bakery owns price, availability and allergens.

02 · Research and requirements

Decisions were made before the build, one question at a time

Before any feature, I ran “grills”: the agent puts each open decision to me, one at a time, with a recommended answer. The grill of 6 July locked 12 product decisions and 8 build phases. Every later ticket opened the same way.

Two of the twelve shaped everything after: size is the only variation axis, with every other choice stored as structured data, “never a text blob”; and the browser “is fast but never trusted”.

Six hard rules open the agents’ manual. One says the plugin must still render correctly with the theme switched off.

Requirements argue from real examples. The deli’s 2025 order board was read cell by cell, and each finding became a rule:

Source: docs/deli/order-board-analysis.md · docs/deli/requirements.md:80 · includes/class-cancellation.php:19-30
What the 2025 workbook showedThe rule it became
A 2 lb side was counted, not doubled: the master showed 9 where the weight was 18 lbCooking totals are computed from order lines and a side-amounts table, never typed
26 of 42 orders were picked up on the last dayPer-day, per-store sheets with slot numbers
Only 20 of 44 guide rows carried the website’s order codeThe order number and slot print on every sheet
11 of 32 bundles took no pie, and 11 took no proteinProtein & Dessert is an optional step with counters
Cancellations were deleted cells with “Cancelled” typed inCancel is a status plus a full refund, and the order is kept

The rule that kept the agents honest is one sentence: “An undocumented assumption is a bug.” Any session that assumes something the docs don’t decide adds a numbered row before it commits, and I sign it off, supersede it or leave it pending.

docs/assumptions-register.md5 of 1,594 rows
#Assumption (trimmed)Decided
A372-hour lead time means “today + 3 calendar days”, not a rolling clock; the hours are editable in SettingsSigned off 2026-07-07
A349Check 18 fails on six seasonal products that carry the Seasonal category but no season: “a finding, not fixed here”Recorded; the data is mine to fix
A960“Nothing prints on its own; a Print click goes on the site’s print list and a runner next to the Epson prints it”José, 2026-10-20
A1306An Undo doesn’t ask the holiday calendar; the checkout gate does“José’s call, as built first”, 2026-10-26
A2032Each holiday side’s allergens sit below its ingredients; a blank one reads “None”José, 2026-11-04

The register holds 1,594 rows, numbered up to A2121 because each ticket prompt reserves a block of numbers.

Figure 1 · Test suite and register over time

The rulebook and the test suite grew together

Left, assertion lines in a full test run. Right, the highest row number in the assumptions register. Same dates, separate scales.

Both took off in October: the register went from A218 to A2121, the suite from 1,156 to 6,089 assertions.
Source: git log -p on tests/cli/README.md and docs/assumptions-register.md, computed 2026-11-07 Data

03 · Insights

Five research findings changed what customers and staff actually do

The research produced 17 findings that changed the build. These five matter most to the people using it.

  1. Run the counter, not the cap

    The bakery had no daily limit to give me, so the cap launched switched off while a counter ran from day one, derived from real orders with no stored total to drift. A real cap can come later, from data.

    includes/class-capacity.php:2-17 · docs/build-roadmap.md:94-96

  2. The cut-off is a minute, not a day

    Lead time moved from 72 hours to two days with a 12:00 pm cut-off: an order placed before noon counts from today, and from noon it counts from tomorrow. A customer who opens the calendar at 11:55 and submits at 12:05 is refused by the server’s recheck, “as designed, no client timer”.

    includes/helpers.php:907-911 · docs/assumptions-register.md:1291-1292 (A1504–A1505)

  3. Pickup comes first

    Step 1 is the pickup: a store, then a calendar with exactly three day states, and the step can’t advance without a date the server marks open. A customer never designs a cake for a day the bakery can’t make.

    docs/figma/figma-final-wireframe-plan.md:37-39 · tests/cli/checks/06-pickup-first.php:3-6

    Step one of the cake builder: a pickup calendar with available, unavailable and selected days, two store cards, and a running summary of the order.
    Builder step 1 at 1280 px: the calendar’s three day states, the store choice and the running summary.
  4. Tickets print on a click, not on payment

    The plan said auto-print, but the web host can’t reach a printer on the store network, and on 20 October I ruled it out: “i dont want tickets to automatically print when an order gets confirmed.” Now a Print click puts the job on a list, and a small runner beside the receipt printer prints it.

    docs/assumptions-register.md:913 (A960) · includes/class-print-agent.php:2-33

  5. One breakpoint, at 782 px

    The storefront CSS has exactly one media query, at 782 px; there’s no tablet layout. A check hashes every byte outside that block, so the 375 px layout I approved on my phone can’t move by accident.

    docs/parity/28/ledger.md:42-43 · tests/cli/checks/33-desktop-pass.php:3-19

04 · Design

The Figma journey was the spec, and every deviation was logged

The flow was designed at 375 px in the co-op’s design-system file, from library tokens and components only, with each backend rule annotated on its frame in Dev Mode.

Thirteen mobile screens in a row in Figma, joined by flow arrows, from the bakery landing to the order confirmation.
The final journey at 375 px: 13 screens from Landing to Confirmed, with the flow drawn between them.

An overnight agent run on 5–6 August built 30 components, a 97-row copy map and 13 screens with 35 annotations. Its audit read 0 raw fills, 0 raw strokes, 0 raw font sizes and 0 raw radii.

I approved the structure and sent back the styling. The diff became a register of 30 deviations, 16 global and 14 per screen, plus seven calls left for me that would be “chipped, not silently decided”. An earlier pass had felt “too big, too bold-y”; the audit traced it to desktop text styles on 375 px frames.

The copywriters edited a duplicate of the journey. A read-only diff compared 691 text layers, found 91 copy edits and 54 comment threads, and changed nothing until I approved: “No node was mutated.”

A Figma sheet of bakery components: tags, option cards, calendar days, headers, menu cards and summary panels.
The component sheet: option cards, a three-state calendar, and one build summary in three variants.
A mobile builder screen with a five-step progress bar, a pickup calendar, two store cards and a Select Size button.
Step 1, Pickup: three day states, store cards, and the price drawer.

The token rule is one line: “Never hand-author a brand value.” The plugin reads 200 --cfc-bakery-* names, and one file, tokens.php, is the only place they meet the theme’s tokens.

In code, every screen was compared with its frame at 375 px in 96 screenshots, until all 13 read “self-review passed”. The last gate was my own phone: “everything passed on my iPhone.”

05 · Build

WooCommerce does the plumbing; 64 modules do the bakery

The plugin doesn’t rebuild commerce. WooCommerce handles payment, cart and checkout; the plugin adds the builder, capacity, order statuses, tickets, emails and the staff screens.

How a bakery order moves through the system A customer builds a cake in the storefront, pickup first, then checks out; the date and store are re-checked at the click and WooCommerce creates the order. Each status change (to make, ready, picked up, or cancelled with a refund) drives the emails, the print list and the staff dashboards. Admin settings, catalogs and imports feed the builder. One token map feeds every surface: live CSS variables on the storefront, a snapshot in admin and print, inline literal values in email. CUSTOMER Orders a cake Phone or laptop, picks up in store STOREFRONT Cake builder Pickup first, then size, flavour, frosting, extras STOREFRONT Cart and checkout Date and store are re-checked at the click WOOCOMMERCE Order and status To make Ready Picked up Cancelled · refund each status change NOTIFICATIONS Emails Confirmed, ready, changed, cancelled PRINTING Print list A click queues a job; a runner prints it STAFF Dashboards Bakery and deli, refresh every 60 s ADMIN · WP-ADMIN Settings 10 tabs Catalogs and sizes 9 catalogs Imports dry run → plan → apply catalogs, sizes, prices DESIGN TOKENS Token map tokens.php, 200 names LIVE INLINE SNAPSHOT SNAPSHOT order data design tokens · LIVE on the storefront, SNAPSHOT in admin and print, INLINE in email cfc-bakery-orders @ f28ff68 How a bakery order moves through the system The customer and the admin set-up both feed the cake builder, pickup first. The cart and checkout re-check the date and store, and WooCommerce creates the order. Each status change (to make, ready, picked up, or cancelled with a refund) drives the emails, the print list and the staff dashboards. One token map feeds every surface: live CSS variables on the storefront, a snapshot in dashboards and print, inline literal values in email. CUSTOMER Orders a cake Phone or laptop, picks up in store ADMIN Set-up Settings, catalogs, sizes, imports STOREFRONT Cake builder Pickup first, then size, flavour, frosting and extras STOREFRONT Cart and checkout Date and store are re-checked at the click WOOCOMMERCE Order and status To make Ready Picked up Cancelled · refund each status change EMAIL Emails Sent once per status PRINT Print list On a click, via a runner STAFF Dashboards Bakery, deli, 60 s refresh DESIGN TOKENS One token map, every surface tokens.php · 200 names Storefront LIVE var() Dashboards, settings SNAPSHOT Print ticket SNAPSHOT Email INLINE · zero var()
Customer → storefront → cart and checkout → WooCommerce order → statuses, which drive emails, the print list and the dashboards. One token map feeds every surface.

There are 64 PHP modules and 50,116 lines, in ten groups:

  • Catalog and modifier engine 11
  • Storefront, cart and checkout 7
  • Capacity and calendar 3
  • Order lifecycle 6
  • Notifications 2
  • Printing 1
  • Staff dashboards 4
  • Holiday deli layer 9
  • Importers 7
  • Settings and Health 14

Theme tokens only exist on front-end pages, so each surface resolves them its own way:

--cfc-bakery-color-inkone of 200 names · includes/tokens.php

  1. Live

    Storefront pages

    var(--wp--preset--*, fallback): the theme’s live value

    var(--wp--preset--color--primary, #284734)

  2. Snapshot

    Staff dashboard and print ticket

    a :root snapshot written into every render

    :root { --cfc-bakery-color-ink: #284734 }

  3. Inline

    Email

    literal values inlined on each element, zero var()

    style="color:#284734"

One token name, three ways to resolve it. Source: docs/design-tokens.md:73-85 · includes/tokens.php:131, :158, :365-392 · assets/css/print.css:4-5

The fallbacks are WordPress’s own computed clamp() strings. theme.json’s size field is the clamp’s maximum, and a fallback built from it would have rendered every email and ticket “too big”.

Figure 2 · Lines by area

The tests outweigh the product code

Lines in the plugin repo by area, 7 November 2026.

62,396 lines of tests guard 50,116 lines of PHP. The docs almost match the code, which the retro counts as a cost.
Source: git ls-files + wc -l, computed 2026-11-07 Data

06 · The AI workflow

Agents built it; a fresh reviewer and my phone decided when it was done

Every commit is authored under my GitHub login, and 1,482 of 1,503 carry a Claude co-author trailer. My part is on either side of the code: the decisions and rules before it, and the verdict after.

How José ships a ticket with AI agents José writes the brief and makes the decisions. An orchestrator agent plans, merges and holds the database lock, and hands each ticket to a builder lane working in its own git worktree. A fresh reviewer scores every change from 1 to 10; 9 or above passes, below 9 goes back for a fix and a new reviewer, at most five rounds. Passing work goes to staging, José checks it on his phone and says "passed", and only José pushes main, the live store. 9/10 Below 9: fix, then a new reviewer · at most 5 rounds HUMAN José Brief and decisions AGENT Orchestrator Plans, merges, holds the lock BUILDER LANES #49 own worktree #50 own worktree #51 own worktree AGENT · FRESH Reviewer Scores 1–10; 9 or above passes ENVIRONMENT Staging Orchestrator pushes here HUMAN Phone check José walks it, says “passed” PRODUCTION main Live store; José pushes How José ships a ticket with AI agents José writes the brief and makes the decisions. An orchestrator agent plans, merges and holds the database lock, and hands each ticket to a builder lane in its own git worktree. A fresh reviewer scores every change from 1 to 10; 9 or above passes, below 9 goes back for a fix and a new reviewer. Passing work goes to staging, José checks it on his phone and says "passed", and only José pushes main, the live store. 9/10 passes José pushes HUMAN José Writes the brief, makes the decisions AGENT Orchestrator Plans, merges lanes, holds the lock BUILDER LANES · OWN WORKTREE EACH #49 worktree #50 worktree #51 worktree AGENT · FRESH Reviewer Scores 1–10; 9 or above passes Below 9: fix, then a new reviewer ENVIRONMENT Staging The orchestrator pushes here HUMAN Phone check José walks it and says “passed” PRODUCTION main The live store; only José pushes it
José → orchestrator → lanes in worktrees → fresh reviewer at 9/10 → staging → phone check → main.

One orchestrator session holds the plan and the shared test database. Builders work in their own git worktrees and may not push staging or main, merge, or edit the status file. On 25 October I handed the deli build to an orchestrator:

“you are the orchestrator … starting as many sessions as you need”

“I will leave this plugged in until you are done”

— José, 2026-10-25 · docs/agents/handoffs/holiday-orchestrator.md:11-14

By the next morning all 18 deli tickets were merged and on staging.

Figure 3 · When commits happened

Nights and Mondays

Commits by hour of day (Pacific) and by weekday, all branches.

45% of commits landed between 10 pm and 3 am: overnight agent runs, reviewed in the morning.
Source: git log --all, local time, computed 2026-11-07 Data

Each deli ticket got its prompt from a separate session. The one for #49, the mixed cart, runs 243 lines under these headings:

docs/agents/session-prompts/holiday-ticket-12-mixed-cart.md243 lines
# #49 (draft ticket 12) — Mixed cart: holiday is the third kind, and the department is decided at checkout## 0. The goal, in José’s words## 1. Blocked by, and what you may assume has landed on `holiday`## 2. What you build (the ticket body on #49 is the contract; this is the order — seams first, screen last)## 3. The `--only` list## 4. Check 47’s `47.12.*` sections (engine seam — library / stored data in → verdict out)## 5. The browser pass on Local (own orders, every tick by own order id)## 6. The review lane — PILLAR## 7. The Local lock, the stop rules, the 40% rule, the hand-back

When a builder passes about 40% of its context window, it stops at a clean commit and writes a hand-off for a fresh session. Hand-backs read like lab notes:

docs/agents/handoffs/ticket-68-hand-back.md:63-73excerpt
Review R1: 8/10, FIX. Two must-fixes […] Fix round 1 (12577ab).Review R2: 9/10, PASS. Browser pass on 12577ab: PASS […] Fix round 2 (ce2e87d).Review R3: 9/10, PASS. […]Full suite on the final tree: 6088 passed / 1 failed theme on, 6083 / 1 theme off (f38fd9e).

The review gate for risky changes is one rule. A new reviewer each round scores the change out of 10; 9 passes and anything lower goes back. The reviewer reruns the tests itself:

“It does not trust the builder’s word.”

docs/agents/review-loop.md:28

I raised the cap from three rounds to five after one ticket needed five (7, 8, 8, 7, 9) and every failed round had found a real bug.

Figure 4 · Review rounds to a pass

Most changes passed review in one or two rounds

Mentions of “9/10 in N rounds” across the status file and the agent records. A ticket can be mentioned more than once, so read the shape, not a count of tickets.

159 of 188 mentions are one or two rounds. Perfect scores are rare: four in the whole record.
Source: grep across STATUS.md, docs/agents and docs/parity, computed 2026-11-07 Data

The test harness asserts at one seam, data in and verdict out, never through a browser, with the theme on and off. After two overlapping runs lost 16 settings on 20 October, every run takes a lock. The one standing red is a data problem of mine, and every report names it.

Figure 5 · GitHub issues, cumulative

69 tickets opened, 61 closed, median nine days to close

Issues opened and closed on the plugin repo, 6 August to 7 November 2026. The shaded gap is open work.

The two jumps are planning days: 14 tickets cut from the spec on 6 August, and 19 opened on 26 October, most of them the deli layer.
Source: gh issue list --state all, 2026-11-07 Data

A bug found outside a ticket gets a row and a question, never a quiet fix: “No fix without José’s word — not even a one-liner.” The status file uses the phrase “record-and-ask” 99 times.

Figure 6 · Co-author trailers by model

1,482 of 1,503 commits carry a Claude co-author trailer

Co-Authored-By trailers by model, all branches.

Fable 5.1 is on 1,051 commits. Opus 5.5 became the builder model in mid-October.
Source: git log --all --format='%(trailers:key=Co-Authored-By,valueonly)', 2026-11-07 Data

Figure 7 · Commits by type

Almost half the commits were documentation

Conventional-commit prefixes across 1,503 commits.

The 734 docs commits are the records that let a fresh session start cold. The retro lists them as waste, and it’s right.
Source: git log --all subject prefixes, computed 2026-11-07 Data

07 · Outcome

Live on 1 November; Thanksgiving orders opened three days later

The roadmap’s target was 15 August. The store went live on 1 November, 78 days later, after the deli layer joined the scope in October.

Figure 8 · Commits per day, all branches

142 days from first commit to a live store

Daily commits from 12 June to 5 November 2026, with milestones. October alone holds 1,174 of 1,503.

The tallest bar is the deli build: 18 tickets in one orchestrated night, 307 commits dated 26 October.
Source: git log --all, computed 2026-11-07 Data

“all passed. push to main”

— José, 2026-11-04 · STATUS.md:179
Source: STATUS.md:161-183
DateLaunch week
1 Nov, eveningLive on production. A round of launch fixes is built overnight in two lanes, both reviewed 9/10
2 NovSite-wide checkout built, reviewed and browser-tested; my five staging notes fixed; on production that evening
3 NovSize price push fails my staging walk, is rebuilt, and reaches production the same day. The holiday walk passes and the 18 deli tickets close
4 NovThanksgiving ordering opens. Nine deli features that skipped scored review get read-only reviews from nine fresh sessions: all 9/10
5 NovA Health tab for staff reaches staging; the suite stands at 6,089 assertions

I’m not showing order volume. The repo doesn’t record it, and the store’s sales are the co-op’s numbers.

08 · What I’d change

It worked, and it cost more process than it needed

I wrote a retro on 4 November and turned it into a playbook. Three things stay, three change, two go.

Keep

  • A fresh reviewer, a 9/10 pass mark and a cap on rounds.“Every failed round found a real bug.”
  • Staging first, then my phone.My phone check caught a pricing bug the review had passed.
  • One decision at a time, with a recommendation,and record-and-ask for anything outside the ticket.

Change

  • Ticket prompts of 3,000 to 7,000 words.The playbook now caps a brief at one page.
  • Tests that pin markup and wording:161 commits did nothing but re-pin them.
  • One shared test database for parallel sessions:it needed a lock and a recovery, and test orders still leaked into Mailchimp.

Drop

  • Essay commits and history in code comments.The average commit subject ran to 243 characters.
  • “Known reds”.Check 18 failed on every run for weeks, for a data reason; next time it gets fixed, not reported.

“the bakery project wrote 1.09 million words of docs (about 12 novels). STATUS reached 99,000 words and contradicted itself, and 45% of the decision rows were never signed off.”

~/ai-playbook/PLAYBOOK.md:216-218