Ruby Autopilot + WhatsApp Escalation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Make Ruby autopilot engage every WhatsApp thread (muted only by a durable human-handoff marker), route reschedule/booking through Ruby’s collect-and-escalate model, fire notification_alert.mp3 as the single notification tone on escalation, and tighten the prompt’s sensitive-data/on-topic guardrails.
Architecture: Server change flips the per-thread gate in runRubyForInbound from “engage only if ruby-on” to “engage unless ruby-off”; the existing escalate() handoff sets that durable ruby-off marker. UI change unifies the notification tone on ui/public/sounds/notification_alert.mp3 and plays it in the already-existing Ruby-escalation branch of NotificationProvider. The reschedule slot-list flow stays gated off (shipped); reschedule falls through to Ruby.
Tech Stack: TypeScript, Cloudflare Workers (Hono), Drizzle ORM, Vitest (server), React + Vite (UI), Web Audio / HTMLAudioElement.
File Structure
| File | Responsibility | Change |
|---|---|---|
server/src/lib/whatsapp-assistant-state.ts | Conversation label helpers + Ruby on/off persistence | Add RUBY_OFF_LABEL, isRubyMuted, pure applyRubyToggle; setRubyOn writes the ruby-off mute marker |
server/src/lib/__tests__/whatsapp-assistant-state.test.ts | State unit tests | Add isRubyMuted + applyRubyToggle tests |
server/src/lib/whatsapp-assistant-dispatch.ts | Ruby dispatch orchestrator + pure guards | Gate on isRubyMuted not isRubyOn; update shouldRubyAutoSend semantics |
server/src/lib/__tests__/whatsapp-assistant-dispatch.test.ts | Dispatch guard unit tests | Update shouldRubyAutoSend cases |
server/src/lib/ai/prompts.ts | Ruby system prompt | Add sensitive-data + on-topic guardrail rules |
server/src/lib/ai/__tests__/prompts.test.ts | Prompt regression test (NEW) | Assert guardrail clauses present |
ui/src/lib/notification-sound.ts | Notification tone | Play notification_alert.mp3; single tone |
ui/src/components/providers/NotificationProvider.tsx | App-wide notification SSE handler | Play alert tone on Ruby escalation |
Task 1: ruby-off durable mute marker in state file
Files:
-
Modify:
server/src/lib/whatsapp-assistant-state.ts -
Test:
server/src/lib/__tests__/whatsapp-assistant-state.test.ts - Step 1: Write the failing tests
server/src/lib/__tests__/whatsapp-assistant-state.test.ts:
- Step 2: Run test to verify it fails
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-state.test.ts
Expected: FAIL — isRubyMuted, applyRubyToggle, RUBY_OFF_LABEL are not exported.
- Step 3: Implement in
whatsapp-assistant-state.ts
export const RUBY_ON_LABEL = 'ruby-on'; through the end of setRubyOn with:
- Step 4: Run test to verify it passes
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-state.test.ts
Expected: PASS (all describe blocks green).
- Step 5: Commit
Task 2: Flip dispatch gate to engage-all-unless-muted
Files:-
Modify:
server/src/lib/whatsapp-assistant-dispatch.ts:16(import),:50-52(shouldRubyAutoSend),:298-299(gate) -
Test:
server/src/lib/__tests__/whatsapp-assistant-dispatch.test.ts - Step 1: Update the failing tests
describe('shouldRubyAutoSend', ...) block in whatsapp-assistant-dispatch.test.ts with:
- Step 2: Run test to verify it fails
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-dispatch.test.ts
Expected: FAIL — shouldRubyAutoSend('autopilot', []) currently returns false (still gated on ruby-on).
- Step 3: Update the import (line 16)
- Step 4: Update
shouldRubyAutoSend(lines 49-52)
- Step 5: Update the per-thread gate in
runRubyForInbound(lines 298-299)
- Step 6: Run tests to verify they pass
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-dispatch.test.ts
Expected: PASS. Then confirm no other references to the removed isRubyOn import remain in this file:
Run: cd server && grep -n "isRubyOn" src/lib/whatsapp-assistant-dispatch.ts
Expected: no output.
- Step 7: Commit
Task 3: Verify reschedule routes through Ruby (no new code)
Files: none modified — verification only. The kill-switch (rescheduleAutoResponderEnabled, default off) shipped in commit 600c97128; with it off, the “reschedule” keyword falls through to inbound handling and (in autopilot) reaches Ruby.
- Step 1: Confirm the kill-switch default is off
cd server && grep -n "rescheduleAutoResponderEnabled === true" src/lib/whatsapp.ts
Expected: one match (returns true only when explicitly set) — i.e. default off.
- Step 2: Confirm both reschedule entry points are gated
cd server && grep -n "isRescheduleAutoResponderEnabled" src/routes/whatsapp-webhook.ts
Expected: two const rescheduleOn = await isRescheduleAutoResponderEnabled(...) guards, each returning only when rescheduleOn is true (otherwise falls through).
- Step 3: Confirm fall-through reaches Ruby
src/routes/whatsapp-webhook.ts around the reschedule block and confirm that when rescheduleOn is false the handler does NOT return, so execution continues to the inbound-logging + runRubyForInbound dispatch near line 1001-1017. No code change. Note this in the commit body of the next task if no standalone commit is warranted.
Task 4: Prompt guardrails — sensitive data + on-topic
Files:-
Modify:
server/src/lib/ai/prompts.ts(thewhatsappAssistanttemplate, in the## Rulessection after### Facts Not in KNOWLEDGE) -
Create:
server/src/lib/ai/__tests__/prompts.test.ts - Step 1: Write the failing regression test
server/src/lib/ai/__tests__/prompts.test.ts:
- Step 2: Run test to verify it fails
cd server && npx vitest run src/lib/ai/__tests__/prompts.test.ts
Expected: FAIL on the sensitive-data and stay-on-topic assertions.
- Step 3: Add the guardrail rules to the prompt
server/src/lib/ai/prompts.ts, inside the whatsappAssistant template, immediately after the ### Facts Not in KNOWLEDGE block (before ### Appointment Lookup), insert:
- Step 4: Run test to verify it passes
cd server && npx vitest run src/lib/ai/__tests__/prompts.test.ts
Expected: PASS (4/4).
- Step 5: Commit
Langfuse note:PROMPT_NAMES.whatsappAssistant = 'whatsapp-assistant'— if a prompt with that name exists in Langfuse it OVERRIDES this hardcoded default at runtime. After deploy, mirror these two rules into the Langfusewhatsapp-assistantprompt (or the guardrails won’t apply for clinics hitting the Langfuse version). The langfuse skill / CLI can update it.
Task 5: Unify the notification tone on notification_alert.mp3
Files:
-
Modify:
ui/src/lib/notification-sound.ts(replace the synthesized chime with the MP3) -
Step 1: Replace
primeNotificationSoundandplayNotificationSound
ui/src/lib/notification-sound.ts, replace the entire body of primeNotificationSound (lines ~74-80) and playNotificationSound (lines ~82-128) — i.e. everything from export function primeNotificationSound(): void { to the end of the file — with:
audioContext variable, the getContext() function, and any AudioContext-only code that is no longer referenced. Keep muted, whatsappInboundSoundEnabled, their storage keys, and the four setter/getter exports (setNotificationSoundMuted, isNotificationSoundMuted, setWhatsappInboundSoundEnabled, isWhatsappInboundSoundEnabled).
- Step 2: Verify no remaining references to the deleted synth symbols
cd ui && grep -n "getContext\|audioContext\|createOscillator\|FREQ_A" src/lib/notification-sound.ts
Expected: no output.
- Step 3: Type-check the UI
cd ui && npx tsc -p tsconfig.app.json --noEmit 2>&1 | grep notification-sound
Expected: no output (no type errors in this file).
- Step 4: Commit
Task 6: Play the alert tone on Ruby escalation (app-wide)
Files:-
Modify:
ui/src/components/providers/NotificationProvider.tsx(add import + one call in the existingisRubyEscalationbranch at lines ~339-350) - Step 1: Add the import
@/lib imports at the top of NotificationProvider.tsx, add:
- Step 2: Play the tone in the escalation branch
eventSource.onmessage handler, change the existing branch:
- Step 3: Type-check the UI
cd ui && npx tsc -p tsconfig.app.json --noEmit 2>&1 | grep NotificationProvider
Expected: no output.
- Step 4: Commit
Task 7: Full verification + deploy
Files: none — verification + deployment.- Step 1: Run the full server test suite
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-state.test.ts src/lib/__tests__/whatsapp-assistant-dispatch.test.ts src/lib/ai/__tests__/prompts.test.ts
Expected: all PASS.
- Step 2: Build the UI (real type-check gate)
cd ui && npm run build 2>&1 | tail -5
Expected: build succeeds; fresh dist/assets/*.js produced (confirm timestamps are from now).
- Step 3: Manual sound check
type:'system' notification titled WhatsApp needs a human (or have a test thread escalate). Confirm notification_alert.mp3 plays app-wide, exactly one tone, and that the global mute toggle suppresses it.
- Step 4: Deploy via the odontox-commit-deploy skill
odontox-commit-deploy skill (stash-foreign → build committed HEAD → deploy server with --env production → pages deploy UI → force-promote canonical → verify). Server changes (dispatch/state/prompts) AND UI changes (sound/provider) both ship.
- Step 5: Verify on the test tenant FIRST, then watch Dental Square
ssh & Associates (test) clinic number and confirm Ruby auto-replies on a fresh thread (autopilot, no ruby-off). Trigger an escalation (e.g. “I want to speak to a person”) and confirm the handoff notification + sound fire and the thread gets a durable ruby-off (Ruby stops auto-replying there). Only then watch Dental Square’s live threads — its 69 label-less conversations will begin auto-engaging.
Self-Review Notes
- Spec coverage: §1 autopilot-all → Tasks 1-2; §2 reschedule via Ruby → Task 3 (verification, no code, by design); §3 escalation sound → Tasks 5-6; §4 guardrails → Task 4. Testing matches the codebase (mocked agent → structural prompt test, not live-model contract tests). Rollout (§Rollout) → Task 7 Step 5.
- No placeholders: every code step shows complete code/commands.
- Type consistency:
RUBY_OFF_LABEL,isRubyMuted,applyRubyToggledefined in Task 1 and consumed in Task 2;shouldRubyAutoSend(mode, labels)signature unchanged;playNotificationSound(options?)signature unchanged (Tasks 5-6). - Blast radius: only clinics already in
autopilot(Dental Square + ssh & Associates test) auto-engage; copilot/off unaffected.

