Ruby Copilot 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: Turn Ruby from an autopilot that texts patients into a copilot that suggests replies to staff (one-click into the composer, never auto-sends), with a clinic mode (off/copilot/autopilot, default copilot) and a per-conversation Ruby on/off toggle.
Architecture: Clinic mode lives in the existing whatsapp_assistant module’s JSON config (no DB migration), back-compat-parsed from the old enabled boolean. A per-conversation ruby-on label (present=on, absent=off, default off) gates autopilot auto-send and replaces ruby-paused. A new on-demand /suggest endpoint reuses the Ruby agent without sending; the UI shows a staff-only suggestion card that populates the composer via a window event.
Tech Stack: Hono (server routes), Drizzle (Postgres app schema), React + TanStack Query (UI), Vitest.
File Structure
server/src/lib/whatsapp-assistant-config.ts— addmodeto config + back-compat parse (MODIFY)server/src/lib/whatsapp-assistant-state.ts—ruby-onlabel helpers (MODIFY)server/src/lib/whatsapp-assistant-dispatch.ts— mode/ruby-onguard inrunRubyForInbound(MODIFY)server/src/routes/whatsapp.ts—/conversations/:id/suggest,/conversations/:id/ruby-toggle, configmode(MODIFY)server/src/lib/__tests__/whatsapp-assistant-config.test.ts— back-compat parse tests (CREATE)server/src/lib/__tests__/whatsapp-assistant-dispatch.test.ts— guard test (MODIFY)ui/src/lib/serverComm.ts—suggestRubyReply,setRubyConversationOn(MODIFY)ui/src/components/chat-v2/thread/RubySuggestionCard.tsx— staff-only suggestion card (CREATE)ui/src/components/chat-v2/composer/Composer.tsx— Suggest button +chat-v2:suggest-uselistener (MODIFY)ui/src/components/chat-v2/thread/RubyThreadToggle.tsx— header on/off toggle (CREATE)docs/api-reference.md— document the two new endpoints + config mode (MODIFY)
Task 1: Clinic mode in assistant config (back-compat)
Files:-
Modify:
server/src/lib/whatsapp-assistant-config.ts -
Test:
server/src/lib/__tests__/whatsapp-assistant-config.test.ts - Step 1: Write the failing test
- Step 2: Run test to verify it fails
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-config.test.ts
Expected: FAIL — mode is undefined / property missing.
- Step 3: Implement mode in config
server/src/lib/whatsapp-assistant-config.ts, change the interface and parser:
saveAssistantConfig to persist mode and set isEnabled from it:
- Step 4: Run test to verify it passes
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-config.test.ts
Expected: PASS (4 tests).
- Step 5: Commit
Task 2: ruby-on label helpers
Files:
-
Modify:
server/src/lib/whatsapp-assistant-state.ts - Step 1: Add the helpers
server/src/lib/whatsapp-assistant-state.ts (keep withLabel/withoutLabel):
addRubyPausedLabel/removeRubyPausedLabel in place for now (Task 5 removes their callers); delete them in Task 5’s cleanup once unused.
- Step 2: Typecheck
cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep whatsapp-assistant-state || echo clean
Expected: clean.
- Step 3: Commit
Task 3: Gate inline auto-send by mode + ruby-on
Files:-
Modify:
server/src/lib/whatsapp-assistant-dispatch.ts -
Test:
server/src/lib/__tests__/whatsapp-assistant-dispatch.test.ts - Step 1: Extend the pure guard test
server/src/lib/__tests__/whatsapp-assistant-dispatch.test.ts:
- Step 2: Run to verify it fails
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-dispatch.test.ts
Expected: FAIL — shouldRubyAutoSend not exported.
- Step 3: Implement the guard + wire it
server/src/lib/whatsapp-assistant-dispatch.ts add the pure helper near shouldRubyEngage:
runRubyForInbound, after loading config (the tag('config', …) line), add the early guard before gathering context:
ruby-paused check inside shouldRubyEngage’s usage: in gatherDispatchContext’s decision, swap the pause check for the ruby-on requirement. Concretely, change the engage call site to also require ruby-on:
RUBY_PAUSED_LABEL branch from shouldRubyEngage (the ruby-on check above supersedes it) — delete the line if (s.labels.includes(RUBY_PAUSED_LABEL)) return { engage: false, reason: 'handed-off' }; and its import.
- Step 4: Run tests
cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-dispatch.test.ts
Expected: PASS (existing shouldRubyEngage tests + 4 new shouldRubyAutoSend tests). Update any shouldRubyEngage test that asserted the handed-off reason — that branch is gone.
- Step 5: Commit
Task 4: Suggest endpoint (on-demand, never sends)
Files:-
Modify:
server/src/routes/whatsapp.ts - Step 1: Add the endpoint
server/src/routes/whatsapp.ts, near the existing POST /assistant/preview and POST /conversations/:id/ruby-resume, add:
server/src/lib/whatsapp-assistant-dispatch.ts, export a small helper that assembles the agent input from a conversation id (reusing gatherDispatchContext + buildKnowledge):
- Step 2: Typecheck
cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "whatsapp.ts|whatsapp-assistant-dispatch" || echo clean
Expected: clean (pre-existing repo errors elsewhere are fine).
- Step 3: Commit
Task 5: Ruby-toggle endpoint + retire ruby-paused
Files:-
Modify:
server/src/routes/whatsapp.ts,server/src/routes/messages.ts,server/src/lib/whatsapp-assistant-state.ts - Step 1: Replace ruby-resume with ruby-toggle
server/src/routes/whatsapp.ts, replace the POST /conversations/:id/ruby-resume handler with:
removeRubyPausedLabel to setRubyOn (or drop it — it’s imported dynamically above).
- Step 2: Staff manual send turns the thread OFF (durable takeover)
server/src/routes/messages.ts around line 1828, replace:
addRubyPausedLabel import at messages.ts:34.
- Step 3: Delete dead ruby-paused helpers
server/src/lib/whatsapp-assistant-state.ts, delete addRubyPausedLabel, removeRubyPausedLabel, and RUBY_PAUSED_LABEL (now unused — confirm with grep).
Run: cd server && grep -rn "RUBY_PAUSED_LABEL\|addRubyPausedLabel\|removeRubyPausedLabel" src/ | grep -v "__tests__"
Expected: no matches.
- Step 4: Typecheck
cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "whatsapp.ts|messages.ts|whatsapp-assistant-state" || echo clean
Expected: no NEW errors vs baseline.
- Step 5: Commit
Task 6: Config route exposes mode
Files:-
Modify:
server/src/routes/whatsapp.ts(theGET/PUT /assistant/confighandlers, ~lines 644-700) -
Step 1: Return + accept
mode
GET /assistant/config handler, include mode in the JSON response (it’s already on the AssistantConfig from getAssistantConfig). In PUT /assistant/config, accept mode in the body and pass it to saveAssistantConfig:
- Step 2: Typecheck + commit
Task 7: UI client functions
Files:-
Modify:
ui/src/lib/serverComm.ts - Step 1: Add client functions
ui/src/lib/serverComm.ts:
- Step 2: Typecheck + commit
Task 8: Suggestion card + composer wiring
Files:-
Create:
ui/src/components/chat-v2/thread/RubySuggestionCard.tsx -
Modify:
ui/src/components/chat-v2/composer/Composer.tsx - Step 1: Create the card
- Step 2: Render the card + listen for use-event in the composer
ui/src/components/chat-v2/composer/Composer.tsx, add a listener mirroring the existing chat-v2:attach effect (after that effect, ~line 135):
<RubySuggestionCard conversationId={conversationId} /> just above the composer textarea row (only when !isPatient and the clinic mode is not ‘off’ — pass a rubyEnabled prop from the parent that already knows the conversation/clinic, or always render for staff and let the endpoint 409). Import it at the top.
- Step 3: Build + commit
Task 9: Per-conversation Ruby toggle (autopilot only)
Files:-
Create:
ui/src/components/chat-v2/thread/RubyThreadToggle.tsx -
Modify: the thread header component that renders conversation actions (locate via
grep -rn "conversationId" ui/src/components/chat-v2/thread/*Header*) - Step 1: Create the toggle
- Step 2: Render it in the thread header — autopilot mode only
<RubyThreadToggle … /> only when the clinic mode is autopilot (the conversation/clinic data the header already loads carries labels and the clinic’s Ruby mode; pass initialOn={labels.includes('ruby-on')}). In copilot/off mode, do not render it.
- Step 3: Build + commit
Task 10: Docs + superadmin parity
Files:-
Modify:
docs/api-reference.md, superadmin WhatsApp view - Step 1: Document endpoints
docs/api-reference.md: POST /whatsapp/conversations/:id/suggest (staff-only Ruby suggestion, never sends) and POST /whatsapp/conversations/:id/ruby-toggle ({on}), plus the mode field on GET/PUT /whatsapp/assistant/config.
- Step 2: Superadmin parity
mode (read-only is fine) so superadmin can see who is on copilot vs autopilot. (Locate via grep -rn "assistant" ui/src/components/**/superadmin* or the platform-tenants view.)
- Step 3: Commit
Self-Review notes
- Spec coverage: clinic mode (T1, T6), on-demand suggest (T4, T8), staff-only card + one-click no-send (T8), per-conversation toggle (T2, T5, T9), autopilot gating (T3), retire ruby-paused (T5), roles via
requireTab('inbox')+ non-patient guard (T4, T5), docs + superadmin parity (T10). setRubyOn/isRubyOn/RUBY_ON_LABELnames are consistent across T2/T3/T5.- After deploy: existing
enabled:trueclinics parse tocopilot→ auto-sends stop with no data migration. Autopilot clinics must additionally toggle threadsruby-on.

