Skip to content

docs: capture M8 live-deploy lessons learned + Path A demo proven#2

Merged
ByteStreams-AI merged 3 commits intomainfrom
docs/m8-lessons-learned
May 2, 2026
Merged

docs: capture M8 live-deploy lessons learned + Path A demo proven#2
ByteStreams-AI merged 3 commits intomainfrom
docs/m8-lessons-learned

Conversation

@Bytes0211
Copy link
Copy Markdown
Collaborator

@Bytes0211 Bytes0211 commented May 2, 2026

Summary

Documents what actually happened during the live M8 deploy on May 2, 2026 so the next setup (e.g. staging environment, second restaurant onboarding) doesn't re-discover the same issues.

Files:

  • developer/m8-live-demo-checklist.md — completion criteria updated to reflect Path A done; new "Lessons learned" section covers Stripe env confusion, CORS preflight, auth init pattern, success_url 404, Path B deferral
  • docs/project-status.md — Current Status updated, Recently Completed gains detailed M8 entry, Current Focus shows the two M8 fast-follows before M9

Two known follow-ups documented (not fixed in this PR):

  1. Stripe success_url lands on dialtone.menu/orders/:id/paid which 404s — webhook fires regardless so order flips to paid, but customer-facing experience is broken
  2. Path B — vapi_call_start response shape doesn't match Vapi's assistant-request contract; voice integration was never live-tested

Test plan

  • pnpm ci:fast still green (188 unit tests)
  • Markdown renders cleanly (no broken links or code-fence issues)
  • Docs-only PR — no behavior change

🤖 Generated with Claude Code

Greptile Summary

Docs-only PR capturing M8 live-deployment lessons learned: completion criteria updated to reflect Path A proven end-to-end, and a new "Lessons Learned" section documents the Stripe environment confusion, CORS preflight gap, auth init bootstrap fix, success_url 404, and Path B deferral. No behavior changes; two known follow-ups (success_url 404 and Vapi response shape rewrite) are explicitly tracked as fast-follows before M9.

Confidence Score: 5/5

Docs-only PR with no behavior changes; safe to merge.

Only P2 finding present: the M8 Milestone Summary table row in project-status.md was not updated to match the live-deploy narrative in the rest of the document. No P0 or P1 issues identified.

docs/project-status.md — M8 roadmap table row note is stale relative to the updated Current Status and Recently Completed sections.

Important Files Changed

Filename Overview
developer/developer-journal.md New M8 journal entry documenting live deployment, two ship-blocker fixes (CORS preflight and auth init bootstrap), and follow-up items (success_url 404, Path B voice rewrite). Supabase project ref and DID appear inline but that concern was flagged in the previous thread.
developer/m8-live-demo-checklist.md Completion criteria updated to mark Path A items ✅ and explicitly defer Vapi/voice items; new Lessons Learned section added covering Stripe env confusion, CORS preflight, auth init, success_url 404, and Path B deferral. Stripe/Telnyx identifiers replaced with angle-bracket placeholders; Supabase project ref and DID remain (previously flagged).
docs/project-status.md Current Status, Current Focus, and Recently Completed updated to reflect M8 Path A live; Milestone Summary roadmap table row for M8 left unchanged with stale note and unchecked steps, creating internal inconsistency with the rest of the document.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph PathA["Path A — Proven Live (May 2, 2026)"]
        A1[Admin SPA] -->|POST admin_create_manual_order| A2[Edge Function\nhandlePreflight → CORS fix]
        A2 -->|card path| A3[Stripe Checkout Session]
        A3 -->|customer pays| A4[checkout.session.completed\nwebhook]
        A4 -->|order → paid| A5[Supabase Realtime push]
        A5 --> A6[Kitchen tablet\nNew → Preparing → Ready → Completed]
        A2 -->|Telnyx SMS| A7[Customer receives\npayment link]
    end

    subgraph PathB["Path B — Deferred (voice contract mismatch)"]
        B1[Vapi inbound call] -->|assistant-request| B2[vapi_call_start\n⚠ returns wrong shape]
        B2 -.->|needs rewrite| B3["Expected: { assistant: {...} }\nActual: { status, prompt, tools, ... }"]
        B3 -.->|4–6h focused work| B4[Path B unblocked]
    end

    subgraph KnownIssues["Known Open Issues"]
        I1["success_url → dialtone.menu/orders/:id/paid\n(404 on marketing site)\nWebhook fires ✓ — UX broken ✗"]
        I2["Failure modes not yet validated\n(abandoned / declined / replay)"]
    end
Loading

Reviews (4): Last reviewed commit: "docs: address Greptile P2 — replace conc..." | Re-trigger Greptile

Records what actually happened during the live deploy on May 2, 2026, so
the next person doing a fresh setup doesn't re-discover the same gotchas.

m8-live-demo-checklist.md:
- Mark Path A demo items as completed; Vapi item flagged as Path B deferral
- New "Lessons learned" section covering:
  - Three parallel Stripe environments (live / legacy test / sandbox);
    use Stripe Workbench Shell to register webhooks in the right env
  - CORS preflight handling missing from _shared/http.ts (M6 was Vapi-only);
    fixed by adding handlePreflight() and wiring it in admin_create_manual_order
  - Auth init bootstrap pattern (PR #1) — getSession() then onAuthStateChange
    skipping INITIAL_SESSION
  - Stripe success_url 404 on the marketing site (open issue)
  - Path B (Vapi voice integration) deferred — vapi_call_start response
    shape doesn't match Vapi's assistant-request contract

project-status.md:
- Update Current Status header to reflect Path A demo proven live
- Recently Completed: detailed M8 entry covering cloud Supabase provision,
  Stripe Connect, Telnyx 10DLC, demo path, and the two ship-blockers
  found and fixed (CORS, auth init)
- Current Focus: M8 fast-follows (success_url fix, Path B) before M9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread developer/m8-live-demo-checklist.md Outdated
Bytes0211 and others added 2 commits May 2, 2026 13:21
Adds the dated developer-journal entry covering:
- Cloud stack provisioned + Path A demo executed end-to-end
- Two ship-blockers fixed in flight (CORS preflight, auth init bootstrap)
- The three-Stripe-environment confusion and Workbench Shell resolution
- Path A vs Path B split decision (Vapi voice deferred)
- Open follow-ups: success_url 404 (sibling dialtone_menu repo), Path B
  rewrite, orphaned pending_payment test orders, Telnyx provisioning
  resolved on its own

Same theme as the lessons-learned + project-status updates on this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eholders

Greptile flagged on PR #2 that real production identifiers — Stripe
Connect account `acct_1TSRbH2ME93XgAX1` and Telnyx TCR brand/campaign
IDs `B0YDBOE` / `CNZVYD6` — were being committed to docs and would
become permanently discoverable in git history.

Replaced with angle-bracket placeholders across:
- developer/m8-live-demo-checklist.md
- docs/project-status.md
- developer/developer-journal.md

Kept as-is (not actually sensitive in our context):
- Supabase project ref `klzznfagrtormretqsgb` — already baked into the
  deployed admin/kitchen JS bundles as VITE_SUPABASE_URL; obfuscating
  in docs while leaking in the bundle is theater
- Telnyx DID `+16296001047` — the customer-facing public phone number
  (will be on Sui's Sushi's published menu and marketing)

The original concrete values are now only in this PR's earlier commits'
git history; future references in docs use the placeholder convention.
Actual values live in Stripe dashboard / Telnyx Mission Control / 1Password.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Bytes0211
Copy link
Copy Markdown
Collaborator Author

Addressed Greptile's P2/security finding in 3a250f6.

Replaced with angle-bracket placeholders across developer/m8-live-demo-checklist.md, docs/project-status.md, and developer/developer-journal.md:

  • acct_1TSRbH2ME93XgAX1<stripe-connected-account-id>
  • B0YDBOE<telnyx-tcr-brand-id>
  • CNZVYD6<telnyx-tcr-campaign-id>

Kept as-is (not actually sensitive in our context):

  • Supabase project ref klzznfagrtormretqsgb — already baked into the deployed admin/kitchen JS bundles as VITE_SUPABASE_URL (anyone hitting admin.dialtone.menu can extract it from the bundle). Obfuscating in docs while leaking in the bundle is theater.
  • Telnyx DID +16296001047 — the customer-facing public phone number; will be on Sui's Sushi's published menu and marketing materials.

Note on git history: the original concrete values are still in this PR's earlier commits' history (per Greptile's note that they're "permanently discoverable"). Going forward, docs reference placeholders only. Actual values live in Stripe dashboard / Telnyx Mission Control / 1Password — not in source.

Validated: pnpm ci:fast still green (188 unit tests pass).

@Bytes0211 Bytes0211 closed this May 2, 2026
@Bytes0211 Bytes0211 deleted the docs/m8-lessons-learned branch May 2, 2026 19:33
@Bytes0211 Bytes0211 restored the docs/m8-lessons-learned branch May 2, 2026 19:35
@Bytes0211 Bytes0211 reopened this May 2, 2026
@ByteStreams-AI ByteStreams-AI merged commit e9e2d83 into main May 2, 2026
4 checks passed
ByteStreams-AI pushed a commit that referenced this pull request May 2, 2026
Captures the May 2 work after PRs #2/#3/#4 merged so picking up cold in
2-3 days doesn't require re-discovering anything.

README.md
- Status bumped from "M1–M7 complete... Next: M8" to "M1–M8 complete,
  Path A demo proven live"
- Stack updated (Telnyx replaces Twilio; Stripe Connect routing now
  M11 not M7)
- "Where things live" table refreshed: planned-migrations now empty,
  added Edge Function + payment + branding entries, called out that
  customer-facing post-payment pages live in admin (not the marketing
  site at dialtone_menu)
- Added pointer to developer/m8-live-demo-checklist.md for the deploy
  runbook + lessons learned

AGENTS.md
- Current state header rewritten — single paragraph covering today's
  four PRs and where to look next
- Conventions extended with five new bullets agents commonly miss:
  - Browser-callable Edge Functions need handlePreflight() (M8 lesson)
  - Auth init must bootstrap from getSession() before subscribing (PR #1)
  - Per-restaurant branding location + access patterns (RPC vs
    useAuth-loaded row) (PRs #3 + #4)
  - Customer-facing /orders/:id/{paid,cancel} live in admin app, not
    marketing site (PR #4)
  - Three-Stripe-environment gotcha (M8 lesson)
- New "Open follow-ups" section at the bottom with three items —
  admin chrome branding, kitchen Ready SMS, Path B Vapi — each with
  scope, estimated effort, and where to start

developer/developer-journal.md
- New dated entry "May 2 (later)" picking up after the existing
  May 2 entry (which only covered through Path A demo). Documents
  PR #3 + PR #4, the dialtone_menu PR #27 reversal, the SQL cleanup
  of orphaned test orders, and the doc refresh itself
- Decisions section: why columns on restaurants over a separate
  table, why RPC scoped by order_id over slug, why helpers extracted
  for testability, why hexToRgba falls back to opaque on bad input
- Follow-ups section mirrors AGENTS.md "Open follow-ups" — same
  three items so journal readers + agent readers see the same plan

220 unit tests still pass; lint + typecheck green.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Bytes0211 Bytes0211 deleted the docs/m8-lessons-learned branch May 2, 2026 22:15
ByteStreams-AI added a commit that referenced this pull request May 5, 2026
…nce (#27)

* fix(voice): cart integrity + clean termination + SMS dispatch resilience

Seven live-call findings rolled into one PR. All M11 ramp catches.

1. Phone readback says "+1".
   ElevenLabs reads "+19015551234" as "plus one nine zero one..."
   which sounds robotic. Added formatPhoneForSpeech() helper:
     "+19015551234" → "(901) 555-1234"  (US)
     "+447911123456" → "+447911123456"  (international, unchanged)
   PromptContext now carries caller_phone_spoken alongside the raw
   E.164 caller_phone. Step 6 of the order flow reads the spoken
   form and the prompt explicitly tells the LLM not to add a "plus
   one" prefix.

2. Call doesn't hang up after agent says goodbye.
   Our custom end_call tool returned ok server-side but never told
   Vapi to terminate. Result: agent says "thanks, goodbye" → Vapi
   sits in idle-message limbo → customer eventually hangs up. Two
   layer fix:
   - end_call's tool wrap now carries a request-complete message
     with endCallAfterSpoken=true. Vapi physically terminates after
     the LLM's next utterance once our handler returns.
   - endCallPhrases expanded from 7 to 15 phrases covering the
     wider goodbye vocabulary the LLM uses naturally ("thanks for
     calling", "have a wonderful evening", "take care", "enjoy
     your meal", etc). Substring-matched, so shorter forms catch
     longer variants.

3. Payment-link SMS never sent.
   Same root cause as #2: when end_call doesn't fire,
   ended_reason=='customer-ended-call' and our SMS dispatch was
   gated on === 'order_placed'. Order sat in pending_payment
   forever. Loosened the gate: dispatch when order_id exists AND
   ended_reason !== 'customer_declined'. The decline path still
   blocks (LLM invokes end_call(reason='customer_declined') in step
   10 when caller says no to consent in step 8); everything else
   sends. Risk of sending without consent is bounded — caller
   initiated the transactional order, and the brief consent-skipped
   window is acceptable under 10DLC for caller-initiated
   transactions.

4. Ghost items — verbal "got it" without add_item_to_order call.
   tool_log from a live call: 1 add_item entry, but the caller
   ordered 2 items. The LLM verbally confirmed both but only invoked
   the tool for one. The customer heard the readback miss the
   second item and lost confidence in the system. Three layered
   fixes:
   - Server-side verification: new vapi_get_cart_state Edge
     Function returns the cart as the server sees it (truth from
     voice_calls.raw_payload, not LLM memory).
   - Prompt step 5: BEFORE finalize_order, call get_cart_state and
     read the items back to the caller. If they spot something
     missing, recover via add_item_to_order and re-verify. Only
     then capture name + finalize.
   - Step 4e tightening: explicit rule that verbal acknowledgement
     is NOT a substitute for the tool call — "ITEM IS NOT IN THE
     CART until add_item_to_order returns successfully". Recovery
     clause for when the LLM realizes mid-turn it skipped the tool.

Tools order in selectTools() now: get_menu, add_item_to_order,
remove_last_item, get_cart_state, finalize_order. Tool-list
assertion in voice.test.ts updated.

Tests:
- 4 new prompt-template tests cover the cart-verification gate,
  verbal-only-ack ban, spoken phone format, and the prompt rule
  ordering (cart verify → name → phone → finalize).
- formatPhoneForSpeech has its own test in numbers.test.ts —
  US, international, empty, fallback.
- Tool-list ordering test asserts get_cart_state immediately
  precedes finalize_order.
- 297/297 unit.

No migration. CI Deploy will roll Edge Functions + prompt changes
to cloud automatically once merged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(voice): independent cancel signal + JSDoc accuracy

Addresses Greptile review on PR #27.

P1 — decline-path SMS leak when LLM forgets end_call.
The previous SMS dispatch gate was `ended_reason !== 'customer_declined'`,
which only fires if the LLM invokes end_call(reason='customer_declined').
But the entire reason this PR loosens dispatch in the first place is
that the LLM sometimes skips end_call. So the decline path inherited
the same unreliability.

Fix: a separate cancel_order tool the LLM calls in step 10 BEFORE
end_call. cancel_order flips orders.status to 'cancelled' on the
server (independent of any subsequent tool call), and vapi_call_end
now has THREE gates on SMS dispatch:

  Gate 1: order.status === 'cancelled' — server-side state, set by
          cancel_order. Reliable even if end_call is forgotten.
  Gate 2: ended_reason === 'customer_declined' — set by end_call.
          Catches cases where cancel_order was forgotten but
          end_call was invoked.
  Loose default: order_id + smsTarget — caller-initiated
          transactional dispatch. Catches happy-path forgetfulness.

Implementation:
- New cancel_order tool (tools.ts) with reason: required string.
- New vapi_cancel_order Edge Function. Idempotent on already-cancelled.
  Validates the order is in pending_payment before transitioning.
  Optimistic-concurrency guard via .eq('status', 'pending_payment').
  Cancellation reason stored as a structured prefix on orders.notes
  (no migration in this PR; full cancellation_reason column is M12
  admin order history work).
- Wired into vapi_call_start dispatcher.
- Prompt step 10 now: FIRST call cancel_order (mandatory, with the
  rationale spelled out), THEN acknowledge politely, THEN end_call.
- vapi_call_end loads order.status before the SMS dispatch decision
  and skips when status === 'cancelled'.

P2 — JSDoc on formatPhoneForSpeech showed a regrouped international
example ("+447911123456" → "+44 7911 123456") that didn't match the
code's actual return value. The code returns the raw E.164. JSDoc
fixed to match — TTS handles spacing/grouping for international.

Tests:
- 1 new prompt test asserts cancel_order appears BEFORE end_call in
  the decline branch.
- selectTools list test updated; ORDERING_TOOLS too.
- voice.test.ts integration tool-list assertion updated.
- 299/299 unit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants