CU-08: AI booking chatbot (the differentiator) on the public booking page
Priority: P1 Accounts/sessions: Anonymous invitee (no login). Host P1 (ravikantguptaofficial@gmail.com) must already own a dedicated event type and have a default availability schedule. Gmail for P1 must be logged in (mail.google.com) and Google Calendar (calendar.google.com) for L3. Invitees use plus-aliases of P1's Gmail. Parallel-safe: Yes — this suite books slots on a dedicated, RUNID-scoped event type and never edits global host availability. Exclusive (rewrites global host availability?): No. Estimated time: 30 minutes. L3 reality checks: Confirmation email lands in P1 Gmail (search by RUNID), and a real Google Calendar event is created at the booked time.
Goal
This suite proves Calendo's headline differentiator: an invitee can book a meeting by talking to an AI in plain English on the public booking page instead of hunting through a grid of time slots. The AI must reply fast (~2s), propose only real available slots that match the host's actual availability (never hallucinated), handle ambiguous and impossible requests gracefully, complete a real booking conversationally, and coexist with the traditional grid (the grid must remain a working fallback, and the AI must never offer a slot the grid does not). A working conversational booking flow is the single most important thing that distinguishes Calendo from Calendly, so this is a top-priority production check.
CRITICAL GROUNDING FINDING — read before you start. In the current production code (
/Users/ravf/projects/calendo/public/booking/index.html), the inline AI chat panel (#ai-chat-prompt) is hardcodedstyle="display:none"with the comment<!-- AI Chat inline (hidden for now) -->, and the floating chat bubble component (/Users/ravf/projects/calendo/src/chat-widget.js,#chatToggle) is NOT imported into the booking page. Nothing in the page JS ever reveals the chat. So on production today, an ordinary invitee sees NO AI chat on the booking page. The conversational backend (POST /api/ai/chat) is fully wired and live. Therefore this runbook has TWO parts: (A) verify whether any AI chat surface is visible to a real invitee (expected: NOT visible — this is itself a P1 finding to FLAG), and (B) force-reveal the inline panel via the browser console so you can still exercise and verify the conversational booking capability end to end. Tag every console-reveal step clearly; the human reviewer must know the capability was tested through a workaround, not through the shipped UX.
Preconditions
- No login required for the booking page itself — it is public. Do NOT attempt any cold login on calendo.dev.
- P1 Gmail session active at https://mail.google.com (for L3 email check). If not logged in, FLAG as a precondition failure per
00-setup-preconditions.mdand stop — do not log in with a password. - P1 Google Calendar session active at https://calendar.google.com, with P1's Google Calendar connected to Calendo (for L3 event check). If not connected, FLAG; L3 GCal becomes "cannot verify."
- Host slug for P1: read it from the dashboard at https://calendo.dev/dashboard/ — the booking link field (
#bookingLinkValue) contains...?user=<HOST_SLUG>. Record<HOST_SLUG>. If you cannot read it, FLAG. - A dedicated 30-minute event type scoped to this RUNID must exist and be active. Create it first via the dashboard (see Steps 1-3). It must use P1's default availability schedule (the backend only computes chat slots from
is_default = 1). - Host availability must have at least one open day in the next 14 days (the backend computes slots for
today .. today+14d). If the host has zero availability, the chat correctly says "No available slots" and the booking-completion checks become N/A — FLAG and note it. - If ANY precondition is missing, flag it and stop for that dependency. Do not improvise (no password logins, no editing global availability).
Test data
- RUNID: pick a fresh UTC token at execution time, e.g.
20260601-1530. Reuse the SAME RUNID for every artifact in this run. - Event type name:
CU08 AI Chat <RUNID>-> auto-slugcu08-ai-chat-<RUNID>(record the actual slug shown after creation; the slugifier lowercases and hyphenates). - Event duration: 30 minutes.
- Invitee (conversational booking): name
CU08 Invitee <RUNID>, emailravikantguptaofficial+inv-<RUNID>@gmail.com. - Invitee (grid fallback booking): name
CU08 Grid <RUNID>, emailravikantguptaofficial+grid-<RUNID>@gmail.com. - Booking page URL:
https://calendo.dev/booking/?user=<HOST_SLUG>&event=cu08-ai-chat-<RUNID>. - Gmail search seed: every confirmation will be addressable by searching
inv-<RUNID>orgrid-<RUNID>in P1's Gmail.
Steps
Part 0 — Set up the dedicated event type (host dashboard)
- Action: Go to the dashboard (https://calendo.dev/dashboard/). Wait for the app shell (
#appLayout) and welcome header (#welcomeHeader) to render. -> Expect: Dashboard loads, sidebar visible with tabs (.sidebar-nav a[data-tab="..."]). If an onboarding wizard (#calendarOnboarding) appears, dismiss/skip it. -> [L1]
- Action: On the Overview, click the "+ New" event-type button (
#overviewCreateEtBtn). In the modal (#editEtModal), set name =CU08 AI Chat <RUNID>(#editEtName), duration = 30 (#editEtDuration), description =CU-08 runbook event(#editEtDesc). Click the save/create button (#editEtSaveBtn). -> Expect: Modal closes; the new event appears in the overview event-type list (#overviewEventTypescontainsCU08 AI Chat <RUNID>). -> [L1]
- Action: Read the dashboard booking link (
#bookingLinkValue) and record<HOST_SLUG>(text afteruser=). Confirm the new event's slug (open the event, or derivecu08-ai-chat-<RUNID>). -> Expect: You have a working booking URLhttps://calendo.dev/booking/?user=<HOST_SLUG>&event=cu08-ai-chat-<RUNID>. -> [L2] Capture screenshot: cu08-eventtype-created.
Part A — Does a real invitee see the AI chat? (expected: NO — flag it)
- Action: Open a fresh/incognito browser context (anonymous invitee, no Calendo cookies). Navigate to the booking page:
https://calendo.dev/booking/?user=<HOST_SLUG>(event-list view). Wait for the host header and event list (#event-types-grid,.event-card) to render. -> Expect: The booking page renders the host name and a "Select a meeting type" list containingCU08 AI Chat <RUNID>. -> [L1]
- Action: Look for any AI chat affordance: a floating "Chat with AI" bubble in the bottom-right (
#chatTogglefrom chat-widget.js), or an inline "Ask AI" panel with an input (#ai-chat-prompt/#ai-chat-inline-input/#ai-chat-inline-send). Check both the event-list view and after selecting the event (#booking-view). -> Expect (per grounding): NO visible AI chat element.#ai-chat-promptis present in the DOM butdisplay:none; no floating bubble exists. -> [L1]- If you DO see a visible chat UI: great — production has been updated since grounding. Record exactly what you see (selector, location) and use it for Part B instead of the console reveal. Note "AI chat visible to invitee = YES" in the report.
- If you do NOT see a chat UI (expected): record FINDING-A: "AI booking chat is not exposed to invitees in production — the differentiator is dark." This is a P1 product gap, not a script error. Proceed to Part B to verify the backend capability still works.
Part B — Reveal the inline chat and verify conversational availability (workaround)
Steps 6-15 use a console workaround to un-hide
#ai-chat-prompt. This exercises the SAME code path real invitees would use once the panel is shown. Tag the whole part as [workaround] in the report.
- Action: Still on
https://calendo.dev/booking/?user=<HOST_SLUG>(event-list view, where#ai-chat-promptlives), open the browser devtools console and run:document.getElementById('ai-chat-prompt').style.display='block'; document.getElementById('ai-chat-prompt').scrollIntoView();-> Expect: A panel appears with the helper text "Not sure which meeting to pick?...", a text input (#ai-chat-inline-input, placeholdere.g. "I need a 30 min call next Tuesday afternoon"), and an "Ask AI" button (#ai-chat-inline-send). -> [L1] [workaround] Capture screenshot: cu08-chat-revealed.
- Action: Type a concrete natural request scoped to a real available day. First check the grid (open the event, note which weekdays show
.calendar-day.available). Then in the chat input type, e.g.,I need 30 minutes on <a known-available weekday> afternoon(e.g. "this Thursday afternoon"). Click "Ask AI" (#ai-chat-inline-send) or press Enter. Start a stopwatch when you send. -> Expect: A "..." typing bubble appears, then within ~2-4 seconds (allow up to ~6s for cold Claude calls) an assistant reply that proposes one or more specific times in 12-hour format ("3:00 PM"), in the host timezone, that are real. Record the reply latency. -> [L1]- Note on rate limits: If the reply is a static fallback like "I can help you book a meeting...! They offer: ... What kind of meeting do you need?" (the event-type list with no times), the Claude API was rate-limited/overloaded (429/529) and fell back. This is an EXTERNAL dependency state, NOT a Calendo bug — record it as
RATE-LIMITED, wait 60s, and retry the same message up to 3 times. If it never returns real times after retries, mark the conversational checksBLOCKED-EXTERNALand still complete the grid path (Part D).
- Note on rate limits: If the reply is a static fallback like "I can help you book a meeting...! They offer: ... What kind of meeting do you need?" (the event-type list with no times), the Claude API was rate-limited/overloaded (429/529) and fell back. This is an EXTERNAL dependency state, NOT a Calendo bug — record it as
- Action: Cross-check every time the bot proposed against the grid. Open the event in a second tab/view (
#booking-view), pick the same date, and read the.time-slotbuttons. -> Expect: Every time the AI offered appears as a real.time-slotfor that date on the grid. The AI must NOT offer a time absent from the grid. -> [L2] Capture screenshot: cu08-grid-matches-chat (grid time slots for the proposed date).
- Action: Pick a proposed slot conversationally — reply in the chat with something like
Yes, 3:00 PM works, book it(use one of the exact times the bot offered). -> Expect: The bot replies with confirmation language ("Great choice! I'll pull up a quick form...") AND an inline mini-form appears in the chat (#ai-book-timedropdown pre-filled with real slot options,#ai-book-name,#ai-book-email,#ai-book-confirm). The slots in#ai-book-timecome directly from the backend's computed real slots. -> [L1]- If no form appears: the backend only shows the form when the reply text contains confirmation phrases AND real slots exist. Try a clearer pick ("book the 3pm slot"). If still no form after a clear pick with known availability, record FINDING-B: conversational confirm did not surface the booking form.
- Action: In the inline form, select the intended time in
#ai-book-time, enter nameCU08 Invitee <RUNID>(#ai-book-name) and emailravikantguptaofficial+inv-<RUNID>@gmail.com(#ai-book-email), then click "Confirm Booking" (#ai-book-confirm). -> Expect: The form replaces itself with a green "✓ Booking confirmed! Check your email for details." and the bot adds a message "Your CU08 AI Chat <RUNID> is booked! A calendar invite has been sent to ravikantguptaofficial+inv-<RUNID>@gmail.com." -> [L1]- Record the booked date/time (host timezone) for L3 — call it
<INV_SLOT>.
- Record the booked date/time (host timezone) for L3 — call it
- Action (persistence): In a host-authenticated tab, go to the dashboard Bookings tab (
.sidebar-nav a[data-tab="bookings"], panel#panel-bookings). Look for the new booking under inviteeCU08 Invitee <RUNID>at<INV_SLOT>. -> Expect: The booking from the AI chat appears in the host's bookings list with the correct invitee name, email, event type, and time. -> [L2] Capture screenshot: cu08-dashboard-booking-from-chat.
Part C — Ambiguity and impossible-request handling
- Action: In the chat (re-reveal
#ai-chat-promptif you navigated away; the conversation session persists vialocalStoragekeycalendo_chat_<HOST_SLUG>), send an ambiguous request:Can we meet next Tuesday?-> Expect: The bot either (a) clarifies (e.g., asks morning vs afternoon, or confirms which Tuesday) or (b) sensibly proposes real available times on the next upcoming Tuesday in 12-hour format. It must NOT invent times outside availability. -> [L1] Capture screenshot: cu08-chat-ambiguous.
- Action: Send an impossible request outside availability:
Book me Sunday at 3am(or any day/time the grid shows as fully unavailable — verify against the grid first). -> Expect: The bot handles it gracefully: it says that time/day is not available and offers the next real available slot(s) instead. It must NOT hallucinate a slot, must NOT confirm a 3am booking, and must NOT produce a booking form for an unavailable time. -> [L1] Capture screenshot: cu08-chat-impossible.
- Action: Send an off-topic message to confirm scope enforcement carries over to the public chat:
What's the weather in San Francisco?-> Expect: The bot declines and redirects to scheduling (consistent with the dashboard AI's scope enforcement). It does not answer the weather. -> [L1] Capture screenshot: cu08-chat-scope.
Part D — Grid fallback coexistence (the traditional path still works)
- Action: As the anonymous invitee, navigate fresh to
https://calendo.dev/booking/?user=<HOST_SLUG>&event=cu08-ai-chat-<RUNID>(no console tricks). Wait for#booking-view. Pick the first available day (.calendar-day.available), then the first.time-slot. Fill#input-name=CU08 Grid <RUNID>,#input-email=ravikantguptaofficial+grid-<RUNID>@gmail.com. Click "Confirm Booking" (#btn-confirm). -> Expect: Confirmation screen with title "Booking Confirmed!" (.confirmation-title), and manage links in#confirmation-manage("Reschedule" ->/booking/reschedule.html?token=..., "Cancel booking" ->/booking/cancel.html?token=...). Record the booked date/time as<GRID_SLOT>and the cancel/reschedule hrefs. -> [L1] [L2] Capture screenshot: cu08-grid-confirmed.
- Action (coexistence assertion): Confirm both paths produced bookings and that the AI never offered a slot the grid lacked. Cross-reference: the AI-offered times (Step 7-9) ⊆ grid
.time-slotset (Step 8); both<INV_SLOT>and<GRID_SLOT>exist as host bookings (Step 11 + Bookings tab). -> Expect: AI path and grid path coexist; AI slots are a subset of grid slots; two distinct bookings exist (one per RUNID alias). -> [L2]
L3 reality checks
Email (Gmail, P1 inbox):
- Open https://mail.google.com (P1 logged in). Search:
inv-<RUNID>. -> Expect: A booking-confirmation email addressed to / referencingravikantguptaofficial+inv-<RUNID>@gmail.comfor the AI-chat booking, subject referencing the event (CU08 AI Chat <RUNID>) and time<INV_SLOT>, body containing the meeting details and Reschedule/Cancel links pointing tocalendo.dev/booking/reschedule.html?token=and.../cancel.html?token=. Capture screenshot: cu08-L3-email-chat. - Search:
grid-<RUNID>. -> Expect: A second confirmation email for the grid booking at<GRID_SLOT>. Capture screenshot: cu08-L3-email-grid.- If the host also receives a separate "new booking" notification in this same inbox, confirm it too; note its presence.
Calendar (Google Calendar, P1):
- Open https://calendar.google.com (P1 logged in). Navigate to the date of
<INV_SLOT>. -> Expect: A real event at exactly<INV_SLOT>(host timezone) titled with the event name (e.g.CU08 AI Chat <RUNID> with <host>), with the inviteeravikantguptaofficial+inv-<RUNID>@gmail.comas an attendee. Capture screenshot: cu08-L3-gcal-chat. - Navigate to the date of
<GRID_SLOT>. -> Expect: A real event at<GRID_SLOT>for the grid booking with attendee...+grid-<RUNID>@gmail.com. Capture screenshot: cu08-L3-gcal-grid.- If P1's Google Calendar is not connected to Calendo, mark GCal L3 "cannot verify — calendar not connected" and rely on Calendo persistence (Step 11) plus email L3.
Cleanup
Leave the host accounts and calendars clean for the next run. Do all of the following:
- Cancel the AI-chat booking: open the Reschedule/Cancel link from the
inv-<RUNID>confirmation email (or/booking/cancel.html?token=<token>recorded earlier), confirm cancellation. -> Expect cancel confirmation page. - Cancel the grid booking: use the cancel link from
<GRID_SLOT>confirmation (Step 15 href). Confirm cancellation. - Verify both are cancelled in the dashboard Bookings tab (status = cancelled or removed from upcoming).
- Delete the dedicated event type
CU08 AI Chat <RUNID>from the dashboard (Event Types tab -> the event -> delete; confirm). This also removes it from the booking page. - Delete the test calendar events if they were not removed automatically by the cancellation: in Google Calendar, delete the
<INV_SLOT>and<GRID_SLOT>events titled with<RUNID>if still present. - Optional Gmail tidy: the plus-alias confirmation/cancellation emails can be left (they are searchable and harmless) or archived. Do not delete other mail.
- Reset the console reveal: none needed — closing/refreshing the incognito context discards the
display:blockoverride; no persistent change was made to production.
Pass/Fail criteria
The run PASSES only if ALL of the following hold (treat external rate-limit as a documented non-failure, see notes):
- Part A finding recorded accurately: whether the AI chat is visible to a real anonymous invitee (expected NO -> FINDING-A flagged; if YES, documented).
- With the chat reachable (revealed or already visible), a natural request returns, within ~6s, an assistant reply proposing real times in 12-hour host-timezone format (not the static fallback). [L1]
- Every AI-proposed time exists as a
.time-sloton the grid for that date; the AI offered NO slot absent from the grid. [L2] - A conversational pick surfaces the inline booking form and a completed form produces "Booking confirmed!" in the chat. [L1]
- The AI-chat booking appears in the host dashboard with correct invitee/email/time. [L2]
- Ambiguous "next Tuesday" -> bot clarifies or proposes real Tuesday slots (no hallucination). [L1]
- Impossible request -> bot declines gracefully, offers a real alternative, creates no booking form/booking for the impossible time. [L1]
- Off-topic message -> bot refuses and redirects to scheduling. [L1]
- Grid fallback path independently completes a booking ("Booking Confirmed!" + manage links). [L1][L2]
- L3: confirmation email for
inv-<RUNID>arrives in P1 Gmail; a real Google Calendar event exists at<INV_SLOT>with the correct invitee attendee (or GCal explicitly marked "cannot verify — not connected"). [L3] - Cleanup completed: both bookings cancelled, event type deleted, stray calendar events removed.
The run FAILS if: the AI proposes a time NOT on the grid (hallucination), confirms an impossible/out-of-availability time, the booking form completes but no booking persists, the grid fallback is broken, or the confirmation email/GCal event never appears while the booking shows confirmed in Calendo. A Claude 429/529 rate-limit fallback is recorded as BLOCKED-EXTERNAL, not FAIL, but the suite cannot PASS the conversational criteria until a non-rate-limited run is achieved.
Evidence to capture
- cu08-eventtype-created, cu08-no-visible-chat, cu08-chat-revealed, cu08-chat-first-reply (with measured latency), cu08-grid-matches-chat, cu08-chat-booking-form, cu08-chat-booking-confirmed, cu08-dashboard-booking-from-chat, cu08-chat-ambiguous, cu08-chat-impossible, cu08-chat-scope, cu08-grid-confirmed, cu08-L3-email-chat, cu08-L3-email-grid, cu08-L3-gcal-chat, cu08-L3-gcal-grid.
- Notes: measured reply latency for Step 7; the exact times the AI proposed vs the grid
.time-slotlist;<INV_SLOT>and<GRID_SLOT>values; FINDING-A (chat visibility) and any FINDING-B; whether any reply was a rate-limit fallback; whether GCal was connected.
Manual residue / cannot-verify
- Product decision, not in-browser verifiable: whether the AI chat should be exposed to invitees (it is intentionally
display:none/"hidden for now"). The agent can only report that it is dark; the human/owner must decide to ship it. This is the single most important hand-off. - Sub-2-second latency SLA: the CLAUDE.md target is "responses under 2 seconds." Real-world latency depends on Anthropic API load; the agent measures and reports actual latency but cannot guarantee the SLA across time.
- Anthropic rate-limit/overload (429/529) and Cloudflare Workers AI fallback quality: external dependency states; the agent records them but cannot control or fully exercise the llama-3.3 fallback path on demand.
- Whether
suggested_slotis correctly timezone-converted for non-host invitee timezones: the inline form builds option labels in the invitee browser's timezone; deep cross-timezone correctness is better covered by a dedicated timezone suite. - AI conversation memory/security across sessions (e.g., whether one invitee's
localStoragesession leaks to another): out of scope here.