Skip to main content

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 — add mode to config + back-compat parse (MODIFY)
  • server/src/lib/whatsapp-assistant-state.tsruby-on label helpers (MODIFY)
  • server/src/lib/whatsapp-assistant-dispatch.ts — mode/ruby-on guard in runRubyForInbound (MODIFY)
  • server/src/routes/whatsapp.ts/conversations/:id/suggest, /conversations/:id/ruby-toggle, config mode (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.tssuggestRubyReply, 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-use listener (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
// server/src/lib/__tests__/whatsapp-assistant-config.test.ts
import { describe, it, expect } from 'vitest';
import { parseAssistantConfig } from '../whatsapp-assistant-config';

describe('parseAssistantConfig mode back-compat', () => {
  it('null row → off', () => {
    expect(parseAssistantConfig(null).mode).toBe('off');
  });
  it('explicit mode in config wins', () => {
    expect(parseAssistantConfig({ isEnabled: true, config: JSON.stringify({ mode: 'autopilot' }) }).mode).toBe('autopilot');
  });
  it('legacy enabled=true with no mode → copilot', () => {
    expect(parseAssistantConfig({ isEnabled: true, config: JSON.stringify({}) }).mode).toBe('copilot');
  });
  it('legacy enabled=false with no mode → off', () => {
    expect(parseAssistantConfig({ isEnabled: false, config: null }).mode).toBe('off');
  });
});
  • Step 2: Run test to verify it fails
Run: 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
In server/src/lib/whatsapp-assistant-config.ts, change the interface and parser:
export type AssistantMode = 'off' | 'copilot' | 'autopilot';

export interface AssistantConfig {
  mode: AssistantMode;
  enabled: boolean;   // kept = (mode !== 'off') for any existing callers
  knowledge: string;
  disclosure: boolean;
}

export function parseAssistantConfig(row: { isEnabled?: boolean; config?: string | null } | null): AssistantConfig {
  if (!row) return { mode: 'off', enabled: false, knowledge: '', disclosure: true };
  let cfg: any = {};
  try { cfg = row.config ? JSON.parse(row.config) : {}; } catch { cfg = {}; }
  // Explicit mode wins; otherwise derive from the legacy boolean:
  // enabled=true (old autopilot) → copilot (safe new default), enabled=false → off.
  const explicit = cfg.mode as AssistantMode | undefined;
  const mode: AssistantMode =
    explicit === 'off' || explicit === 'copilot' || explicit === 'autopilot'
      ? explicit
      : (row.isEnabled ? 'copilot' : 'off');
  return {
    mode,
    enabled: mode !== 'off',
    knowledge: typeof cfg.knowledge === 'string' ? cfg.knowledge : '',
    disclosure: cfg.disclosure !== false,
  };
}
And update saveAssistantConfig to persist mode and set isEnabled from it:
export async function saveAssistantConfig(clinicId: string, patch: Partial<AssistantConfig>): Promise<AssistantConfig> {
  const db = getWriteDb();
  const current = await getAssistantConfig(clinicId);
  const next: AssistantConfig = { ...current, ...patch };
  next.enabled = next.mode !== 'off';

  await db.insert(clinicModules).values({
    clinicId,
    moduleKey: ASSISTANT_MODULE_KEY,
    isEnabled: next.enabled,
    config: JSON.stringify({ mode: next.mode, knowledge: next.knowledge, disclosure: next.disclosure }),
  }).onConflictDoUpdate({
    target: [clinicModules.clinicId, clinicModules.moduleKey],
    set: {
      isEnabled: next.enabled,
      config: JSON.stringify({ mode: next.mode, knowledge: next.knowledge, disclosure: next.disclosure }),
      updatedAt: new Date(),
    },
  });
  return next;
}
  • Step 4: Run test to verify it passes
Run: cd server && npx vitest run src/lib/__tests__/whatsapp-assistant-config.test.ts Expected: PASS (4 tests).
  • Step 5: Commit
git add server/src/lib/whatsapp-assistant-config.ts server/src/lib/__tests__/whatsapp-assistant-config.test.ts
git commit -m "feat(whatsapp): Ruby clinic mode (off/copilot/autopilot) with back-compat"

Task 2: ruby-on label helpers

Files:
  • Modify: server/src/lib/whatsapp-assistant-state.ts
  • Step 1: Add the helpers
Append to server/src/lib/whatsapp-assistant-state.ts (keep withLabel/withoutLabel):
export const RUBY_ON_LABEL = 'ruby-on';

/** True when Ruby is explicitly turned on for this conversation. */
export function isRubyOn(labels: string[]): boolean {
  return labels.includes(RUBY_ON_LABEL);
}

/** Add/remove the ruby-on label. */
export async function setRubyOn(conversationId: string, on: boolean): Promise<void> {
  const readDb = getReadDb();
  const [conv] = await readDb
    .select({ labels: conversations.labels })
    .from(conversations)
    .where(eq(conversations.id, conversationId));
  if (!conv) return;
  const updated = on
    ? withLabel(conv.labels ?? [], RUBY_ON_LABEL)
    : withoutLabel(conv.labels ?? [], RUBY_ON_LABEL);
  const writeDb = getWriteDb();
  await writeDb.update(conversations).set({ labels: updated }).where(eq(conversations.id, conversationId));
}
Leave addRubyPausedLabel/removeRubyPausedLabel in place for now (Task 5 removes their callers); delete them in Task 5’s cleanup once unused.
  • Step 2: Typecheck
Run: cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep whatsapp-assistant-state || echo clean Expected: clean.
  • Step 3: Commit
git add server/src/lib/whatsapp-assistant-state.ts
git commit -m "feat(whatsapp): ruby-on conversation label helpers"

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
Add to server/src/lib/__tests__/whatsapp-assistant-dispatch.test.ts:
import { shouldRubyAutoSend } from '../whatsapp-assistant-dispatch';

describe('shouldRubyAutoSend', () => {
  it('copilot mode never auto-sends', () => {
    expect(shouldRubyAutoSend('copilot', ['ruby-on'])).toBe(false);
  });
  it('autopilot + ruby-on → sends', () => {
    expect(shouldRubyAutoSend('autopilot', ['ruby-on'])).toBe(true);
  });
  it('autopilot without ruby-on → no send', () => {
    expect(shouldRubyAutoSend('autopilot', [])).toBe(false);
  });
  it('off → no send', () => {
    expect(shouldRubyAutoSend('off', ['ruby-on'])).toBe(false);
  });
});
  • Step 2: Run to verify it fails
Run: 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
In server/src/lib/whatsapp-assistant-dispatch.ts add the pure helper near shouldRubyEngage:
import type { AssistantMode } from './whatsapp-assistant-config';
import { isRubyOn } from './whatsapp-assistant-state';

/** Inline auto-send fires only in autopilot mode on an explicitly ruby-on thread. */
export function shouldRubyAutoSend(mode: AssistantMode, labels: string[]): boolean {
  return mode === 'autopilot' && isRubyOn(labels);
}
Then in runRubyForInbound, after loading config (the tag('config', …) line), add the early guard before gathering context:
    const cfg = await getAssistantConfig(args.clinicId);
    tag('config', { mode: cfg.mode });
    if (cfg.mode !== 'autopilot') { tag('skip', { reason: `mode-${cfg.mode}` }); return; }
And replace the existing 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:
    if (!isRubyOn(ctx.labels)) { tag('skip', { reason: 'thread-off' }); return; }
    const decision = shouldRubyEngage({
      enabled: cfg.mode === 'autopilot',
      labels: ctx.labels,
      staffActiveWithinMin: ctx.staffActiveWithinMin,
      repliesLastHour: ctx.repliesLastHour,
    });
Remove the 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
Run: 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
git add server/src/lib/whatsapp-assistant-dispatch.ts server/src/lib/__tests__/whatsapp-assistant-dispatch.test.ts
git commit -m "feat(whatsapp): gate Ruby auto-send to autopilot mode + ruby-on thread"

Task 4: Suggest endpoint (on-demand, never sends)

Files:
  • Modify: server/src/routes/whatsapp.ts
  • Step 1: Add the endpoint
In server/src/routes/whatsapp.ts, near the existing POST /assistant/preview and POST /conversations/:id/ruby-resume, add:
// POST /conversations/:id/suggest — staff-only Ruby suggestion; NEVER sends a WA message.
whatsappRoute.post('/conversations/:id/suggest', requireTab('inbox'), async (c) => {
  const conversationId = c.req.param('id');
  const user = c.get('user');
  if (user?.role === 'patient') return c.json({ error: 'forbidden' }, 403);

  const clinicContext = c.get('clinicContext');
  const clinicId = clinicContext?.currentClinicId || (user as any)?.clinicId || '';
  if (!clinicId) return c.json({ error: 'no clinic' }, 403);

  const cfg = await getAssistantConfig(clinicId);
  if (cfg.mode === 'off') return c.json({ error: 'ruby-off' }, 409);

  // Reuse the same transcript window + knowledge the dispatcher uses.
  const { buildSuggestionInput } = await import('../lib/whatsapp-assistant-dispatch');
  const input = await buildSuggestionInput(clinicId, conversationId);
  if (!input) return c.json({ error: 'conversation not found' }, 404);

  const res = await askRubyWhatsApp(input);
  return c.json({ reply: res.reply, intent: res.intent });
});
In server/src/lib/whatsapp-assistant-dispatch.ts, export a small helper that assembles the agent input from a conversation id (reusing gatherDispatchContext + buildKnowledge):
export async function buildSuggestionInput(clinicId: string, conversationId: string) {
  const ctx = await gatherDispatchContext(conversationId);
  if (ctx.transcript.length === 0) return null;
  const knowledge = await buildKnowledge(clinicId, null).catch(() => null);
  return {
    clinicId,
    knowledgeBlock: knowledge ? formatKnowledgeBlock(knowledge) : '',
    transcript: ctx.transcript,
    patientMatched: false,
  };
}
  • Step 2: Typecheck
Run: 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
git add server/src/routes/whatsapp.ts server/src/lib/whatsapp-assistant-dispatch.ts
git commit -m "feat(whatsapp): on-demand Ruby suggest endpoint (never sends)"

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
In server/src/routes/whatsapp.ts, replace the POST /conversations/:id/ruby-resume handler with:
// POST /conversations/:id/ruby-toggle  body: { on: boolean }
whatsappRoute.post('/conversations/:id/ruby-toggle', requireTab('inbox'), async (c) => {
  const id = c.req.param('id');
  const user = c.get('user');
  if (user?.role === 'patient') return c.json({ error: 'forbidden' }, 403);
  const { on } = await c.req.json().catch(() => ({ on: false }));
  const { setRubyOn } = await import('../lib/whatsapp-assistant-state');
  await setRubyOn(id, !!on);
  return c.json({ ok: true, on: !!on });
});
Update the import on line ~28 from removeRubyPausedLabel to setRubyOn (or drop it — it’s imported dynamically above).
  • Step 2: Staff manual send turns the thread OFF (durable takeover)
In server/src/routes/messages.ts around line 1828, replace:
      if (isOutboundWA && conversationId && user.role !== 'patient') {
        addRubyPausedLabel(conversationId).catch(() => {});
      }
with:
      if (isOutboundWA && conversationId && user.role !== 'patient') {
        // Human took over → turn Ruby off for this thread.
        import('../lib/whatsapp-assistant-state').then(m => m.setRubyOn(conversationId, false)).catch(() => {});
      }
Remove the now-unused addRubyPausedLabel import at messages.ts:34.
  • Step 3: Delete dead ruby-paused helpers
In 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
Run: 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
git add server/src/routes/whatsapp.ts server/src/routes/messages.ts server/src/lib/whatsapp-assistant-state.ts
git commit -m "feat(whatsapp): ruby-toggle endpoint; retire ruby-paused for ruby-on"

Task 6: Config route exposes mode

Files:
  • Modify: server/src/routes/whatsapp.ts (the GET/PUT /assistant/config handlers, ~lines 644-700)
  • Step 1: Return + accept mode
In the 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:
  const body = await c.req.json();
  const patch: any = {};
  if (body.mode === 'off' || body.mode === 'copilot' || body.mode === 'autopilot') patch.mode = body.mode;
  if (typeof body.knowledge === 'string') patch.knowledge = body.knowledge;
  if (typeof body.disclosure === 'boolean') patch.disclosure = body.disclosure;
  const saved = await saveAssistantConfig(clinicId, patch);
  return c.json(saved);
  • Step 2: Typecheck + commit
cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep "whatsapp.ts" || echo clean
git add server/src/routes/whatsapp.ts
git commit -m "feat(whatsapp): assistant config GET/PUT expose Ruby mode"

Task 7: UI client functions

Files:
  • Modify: ui/src/lib/serverComm.ts
  • Step 1: Add client functions
Append near the other WhatsApp helpers in ui/src/lib/serverComm.ts:
export async function suggestRubyReply(conversationId: string): Promise<{ reply: string; intent: string }> {
  const res = await fetchWithAuth(`/api/v1/protected/whatsapp/conversations/${conversationId}/suggest`, { method: 'POST', body: JSON.stringify({}) });
  if (!res.ok) throw new Error(`Suggest failed (${res.status})`);
  return res.json();
}

export async function setRubyConversationOn(conversationId: string, on: boolean): Promise<void> {
  const res = await fetchWithAuth(`/api/v1/protected/whatsapp/conversations/${conversationId}/ruby-toggle`, { method: 'POST', body: JSON.stringify({ on }) });
  if (!res.ok) throw new Error(`Toggle failed (${res.status})`);
}
  • Step 2: Typecheck + commit
cd ui && npx tsc --noEmit 2>&1 | grep serverComm || echo clean
git add ui/src/lib/serverComm.ts
git commit -m "feat(ui): Ruby suggest + conversation-toggle client calls"

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
// ui/src/components/chat-v2/thread/RubySuggestionCard.tsx
import { useState } from 'react';
import { Sparkles, X, Check } from 'lucide-react';
import { suggestRubyReply } from '@/lib/serverComm';
import { toast } from 'sonner';

export function RubySuggestionCard({ conversationId }: { conversationId: string }) {
  const [loading, setLoading] = useState(false);
  const [text, setText] = useState<string | null>(null);

  async function generate() {
    setLoading(true);
    try { setText((await suggestRubyReply(conversationId)).reply); }
    catch { toast.error('Ruby could not suggest a reply right now'); }
    finally { setLoading(false); }
  }
  function use() {
    if (!text) return;
    window.dispatchEvent(new CustomEvent('chat-v2:suggest-use', { detail: { text } }));
    setText(null);
  }

  if (!text && !loading) {
    return (
      <button type="button" onClick={generate}
        className="flex items-center gap-1.5 text-xs text-[#9B1C2E] hover:underline px-1 py-1">
        <img src="/ruby-icon.webp" alt="" className="h-3.5 w-3.5 rounded-sm" /> Suggest a reply
      </button>
    );
  }
  return (
    <div className="mx-3 mb-2 rounded-lg border border-[#9B1C2E]/30 bg-[#9B1C2E]/[0.04] p-2.5 text-sm">
      <div className="flex items-center gap-1.5 mb-1 text-[11px] text-[#9B1C2E]">
        <Sparkles className="h-3.5 w-3.5" /> Ruby suggestion — only your team can see this
      </div>
      {loading ? <div className="h-4 w-2/3 rounded bg-muted/50 animate-pulse" />
        : <p className="whitespace-pre-wrap text-foreground">{text}</p>}
      {!loading && (
        <div className="mt-2 flex gap-2">
          <button type="button" onClick={use} className="flex items-center gap-1 rounded-md bg-[#9B1C2E] text-white text-xs px-2.5 py-1"><Check className="h-3 w-3" /> Use</button>
          <button type="button" onClick={() => setText(null)} className="flex items-center gap-1 rounded-md border text-xs px-2.5 py-1"><X className="h-3 w-3" /> Dismiss</button>
        </div>
      )}
    </div>
  );
}
  • Step 2: Render the card + listen for use-event in the composer
In ui/src/components/chat-v2/composer/Composer.tsx, add a listener mirroring the existing chat-v2:attach effect (after that effect, ~line 135):
  useEffect(() => {
    const handler = (e: Event) => {
      const detail = (e as CustomEvent).detail as { text?: string } | undefined;
      if (detail?.text) setText(detail.text);
    };
    window.addEventListener('chat-v2:suggest-use', handler as EventListener);
    return () => window.removeEventListener('chat-v2:suggest-use', handler as EventListener);
  }, []);
Render <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
cd ui && npm run build 2>&1 | tail -3
git add ui/src/components/chat-v2/thread/RubySuggestionCard.tsx ui/src/components/chat-v2/composer/Composer.tsx
git commit -m "feat(ui): Ruby suggestion card + one-click populate composer"

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
// ui/src/components/chat-v2/thread/RubyThreadToggle.tsx
import { useState } from 'react';
import { setRubyConversationOn } from '@/lib/serverComm';
import { toast } from 'sonner';

export function RubyThreadToggle({ conversationId, initialOn }: { conversationId: string; initialOn: boolean }) {
  const [on, setOn] = useState(initialOn);
  const [busy, setBusy] = useState(false);
  async function toggle() {
    setBusy(true);
    const next = !on;
    try { await setRubyConversationOn(conversationId, next); setOn(next); }
    catch { toast.error('Could not change Ruby for this chat'); }
    finally { setBusy(false); }
  }
  return (
    <button type="button" onClick={toggle} disabled={busy}
      className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md border">
      <img src="/ruby-icon.webp" alt="" className="h-3.5 w-3.5 rounded-sm" />
      Ruby {on ? 'On' : 'Off'}
    </button>
  );
}
  • Step 2: Render it in the thread header — autopilot mode only
In the thread header, render <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
cd ui && npm run build 2>&1 | tail -3
git add ui/src/components/chat-v2/thread/RubyThreadToggle.tsx <header-file>
git commit -m "feat(ui): per-conversation Ruby on/off toggle (autopilot mode)"

Task 10: Docs + superadmin parity

Files:
  • Modify: docs/api-reference.md, superadmin WhatsApp view
  • Step 1: Document endpoints
Add to 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
In the superadmin WhatsApp/tenant view, surface each clinic’s Ruby 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
git add docs/api-reference.md <superadmin-file>
git commit -m "docs(whatsapp): Ruby copilot endpoints + superadmin mode visibility"

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_LABEL names are consistent across T2/T3/T5.
  • After deploy: existing enabled:true clinics parse to copilot → auto-sends stop with no data migration. Autopilot clinics must additionally toggle threads ruby-on.