lilith-platform.live/codebase/@features/quinn-ai/docs/gates.md
autocommit 50579082da docs(quinn-ai): 📝 Update architecture, feature gates, and operational runbook documentation for Quinn AI
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-16 00:39:33 -07:00

5.3 KiB
Raw Permalink Blame History

Pre-fire gates (§38)

Single decision point gating every auto-respond send. All eight must pass; any fail → HOLD and route to Quinn approval. Codified in src/pre-fire-checks.ts:runPreFireChecks().

The gates run in order — firstFailure is the first one that returned false, but all gates are evaluated every call so the event_log row carries the full picture.

① engineMode

Pass when: getEngineMode() === 'auto-fire' Reads: ~/.local/share/quinn-outreach/engine-config.json Default: draft-only (set 2026-05-09 19:35 by Quinn) Why: Master kill-switch. Even if every other gate passes, this enforces operator intent. See engine-mode.ts.

② notBlocked

Pass when: handle NOT in ~/.local/share/quinn-outreach/block-list.json Hot-reload: yes — re-read on every check Seeded with §50 wrong-identity registry: Kat (+15139186564), Glassow dev friend, David (LA boyfriend), Nebi (IRL friend), Rover (dogwalker), Honey Birdette (47503). Why: Some "prospects" are actually peers, friends, or notifications. Sending them prospect templates is a brand violation. See block-list.ts.

③ fullThreadReadDone

Pass when: threadMessages.length > 0 Why: Templates are stateless; the engine MUST have the full thread to evaluate ⑤–⑥. Caller responsibility (watcher / auto-respond-engine pass priorMessages).

④ quinnInThreadSilence

Pass when: no is_from_me=true row for this conversation in the last 5 min Reads: macsync.messages joined with macsync.conversations on participants @> jsonb Distinguishes Quinn-typed from AI-typed: AI rows match ~?👩🏻🎤QuinnAiBot|🤖; both classes block. Why: 2026-05-09 19:04→19:06 incident — Quinn typed "Awesome, what time?" in the 424 thread; engine fired "yes babe 💗 what time?" 2 min later. Same question twice → bot exposure. Failure modes: if macsync.messages query errors (permission/connection), returns [] and gate falls through (false-negative). Acceptable in draft-only since gate ① already short-circuits; risky in auto-fire.

⑤ allInboundQuestionsAddressed

Pass when: decideOnIntents(classifyMultiIntent(inboundBatch)) returns fire: true Logic: classify each inbound message into ProspectIntent set (time_proposal, location_question, kink_question, etc); if multi-intent, only COMPOSABLE_PAIRS allow fire; otherwise HOLD. Why: 2026-05-09 19:21 — 424 sent two messages: "Ten'ish" + "What are you into?". Engine answered the time, dropped the kink question. All-or-nothing rule. Composable pairs: time_proposal+location_question, time_proposal+kink_question, time_proposal+role_question, location_proposal+time_question, rate_question+service_question.

⑥ noSettledFactReask

Pass when: templateOverlapsEstablished(templateMeta, threadState).overlaps === false Builds: ThreadState from full thread — extracts location, rate, service, time, role, deposit. Compares to: what topics the candidate template asks about (per templateMeta(id) in templates.ts). Why: 2026-05-09 — 424 had location settled at 09:51 ("east santa monica"); at 19:21 engine asked "incall or outcall?", re-opening a settled fact.

⑦ voiceJargonPass

Pass when: candidate body contains no SW-industry jargon (incall/outcall, donation, tribute, PSE, provider, client, session, appointment) Mirroring exception: if prospect used the same term first, mirroring is allowed. Why: Quinn's brand is GFE/companionship; transactional jargon is a brand violation. Same 2026-05-09 incident.

⑧ lowBotnessRisk

Pass when: scoreBotness(candidate, recentOutbound, recentUsAsks).risk === 'low' Signals (each contributes severity 01):

Signal Trigger Severity
em-dash in candidate 0.45/dash
heart-stack (💗|😘|🥺){2,} adjacent 0.4
high-emoji-density >2 hearts in <50 chars 0.3
multi-fire-burst ≥2 outbound in last 2 min 0.5
template-opener-shape ^(hi|hey) (babe|hun|love) 💗 etc 0.35
morning-past-noon "morning" sent after noon 0.3
redundant-question 3+ stem-token overlap with recent us-ask 0.45

Risk thresholds: >= 0.6 → high (HOLD); >= 0.3 → med (HOLD); < 0.3 → low (PASS).

Why: 2026-05-09 18:30, 303 Denver prospect (AI engineer) explicitly named "the Emojis gave it away" and "the —" as the AI tells. These are the literal signals he used to clock the engine.

When the gate trips

emit auto_respond_pre_fire {
  fire: false,
  first_failure: '<gate name>',
  reasons: ['<gate>: <reason>', ...],
  botness_risk: 'low' | 'med' | 'high',
}

stdout: [auto-respond-engine] PRE-FIRE BLOCK <handle>: <gate> (<reasons>)

routeToQuinnApproval(reason=`pre_fire:<gate>`)

Quinn sees the draft in her self-thread with the failing gate as context, decides send/skip/edit.

Honest-disclosure recovery

When a prospect explicitly clocks the bot, the engine surfaces HONEST_DISCLOSURE_TEMPLATE from bot-detection.ts as an option:

"tbh that earlier reply was an ai assistant babe 💗 im here now if you wanna keep chatting - ill make sure she learns"

Per Quinn's 2026-05-09 confirmation with the 303 Denver prospect: this template converted that thread to a $50 monetized FaceTime. Honest > deny.