# Runbook ## Default state Engine ships in `draft-only` mode. **Nothing auto-sends until the re-arm sequence below is run.** The kill-switch (gate โ‘  in [gates.md](gates.md)) enforces this even if a downstream caller forgets to consult engine mode independently. ```bash ./run quinn-outreach mode get # mode: draft-only # updatedBy: quinn # reason: 2026-05-09 19:35 directive: stop all auto, only draft ``` ## Re-arming sequence (when Quinn is ready to auto-fire) Always go through this sequence โ€” never skip the dry-run step. ```bash # 1. Verify gates work as expected against recent inbound ./run quinn-outreach dry-run --days=7 --output=/tmp/dry-run-report.txt # Read the report; confirm no obvious false-positives / false-negatives. # 2. Optionally enable booking + scheduled-send first (no auto-fire risk; # these only emit drafts and dispatch already-Quinn-approved sends) systemctl --user enable --now quinn-outreach-booking.service systemctl --user enable --now quinn-outreach-scheduled-send.service # 3. Enable watcher + parser (Quinn-approval flow only โ€” still no auto-fire) systemctl --user enable --now quinn-outreach-watcher.service systemctl --user enable --now quinn-outreach-parser.service # 4. Flip mode to auto-fire WITH ATTRIBUTION (so audit log shows who/why) ./run quinn-outreach mode set auto-fire --by=quinn --reason="re-arm 2026-05-XX" # 5. Enable autorespond engine (now actually dispatches subject to all 8 gates) systemctl --user enable --now quinn-outreach-autorespond.service ``` ## Kill-switch (any time) ```bash ./run quinn-outreach mode set draft-only --by=quinn --reason="" ``` Effective on the next gate evaluation (no daemon restart needed). Already-scheduled sends in the queue **will still fire** โ€” to halt those: ```bash ./run quinn-outreach scheduled-send list --status=pending ./run quinn-outreach scheduled-send cancel ``` For total halt: ```bash systemctl --user stop quinn-outreach-autorespond.service systemctl --user stop quinn-outreach-scheduled-send.service ``` ## Adding a wrong-identity contact (mid-conversation) Discovered Kat is a friend, not a prospect? Block immediately: ```bash ./run quinn-outreach block +15139186564 "Kat - trans peer / Cincinnati piercer" ``` Hot-reloaded; takes effect on next inbound from that handle. No restart. ## Verifying the system ```bash ./run quinn-outreach status # heartbeat / event-log / awaiting-quinn ./run quinn-outreach mode get # current mode + when/who ./run quinn-outreach list-blocked # current block-list ./run quinn-outreach calendar show # tour calendar slots ./run quinn-outreach test # unit tests bun run typecheck # type integrity bun run build # rebuild dist/ ``` ## Incident response: bot-exposed Symptom: prospect explicitly names AI tells (em-dash, heart-stack, multi-fire burst). ```bash # 1. Kill-switch ./run quinn-outreach mode set draft-only --by=quinn --reason="bot-exposed in thread" # 2. Surface honest disclosure template (don't deny) # Template: "tbh that earlier reply was an ai assistant babe ๐Ÿ’— im here now if # you wanna keep chatting - ill make sure she learns" # Source: src/bot-detection.ts โ†’ HONEST_DISCLOSURE_TEMPLATE # Per Quinn 2026-05-09: confirmed conversion to $50 FaceTime after disclosure # 3. Audit which gate failed to catch it (if any) psql -d quinn_icloud -c " SELECT created_at, payload FROM outreach.event_log WHERE event_type = 'auto_respond_pre_fire' AND payload->>'handle' = '' ORDER BY created_at DESC LIMIT 10" # 4. If a gate is missing a signal, add it to bot-detection.ts and ship ``` ## Incident response: stuck in draft-only forever If `mode get` shows `draft-only` but Quinn wants to re-arm, check that the file is writable: ```bash ls -la ~/.local/share/quinn-outreach/engine-config.json ./run quinn-outreach mode set auto-fire --by=quinn --reason="re-arm" ./run quinn-outreach mode get # confirm ``` If the file is corrupt: ```bash rm ~/.local/share/quinn-outreach/engine-config.json ./run quinn-outreach mode set draft-only --by=quinn --reason="reset after corruption" ``` (Default on missing file is `draft-only`, so removing it fails safe.) ## Incident response: ยง37 returning false-negative If `auto_respond_pre_fire` events show gate โ‘ฃ never failing despite Quinn actively typing: ```bash # Check macsync.messages query is reachable psql -d quinn_icloud -c "SELECT count(*) FROM macsync.messages WHERE is_from_me = true AND sent_at > now() - interval '1 hour'" # Verify rich-content visibility (mac-sync v2): are URL-bubble / reaction # messages reaching the classifier, or are they stuck as undecodable? psql -d quinn_icloud <<'SQL' SELECT COUNT(*) FILTER (WHERE body != '') AS plain_body, COUNT(*) FILTER (WHERE body = '' AND body_decoded IS NOT NULL) AS rich_only, COUNT(*) FILTER (WHERE body = '' AND body_decoded IS NULL) AS undecodable FROM macsync.messages WHERE sent_at > now() - interval '24 hours'; SQL # If rich_only is large and undecodable is small, the attributedBody heuristic # is doing its job. If undecodable is large, the typedstream decoder backfill # (see mac-sync migration 2026-05-13_message_attributed_body) has not run. # If permission denied: engine cred lacks icloud SELECT. # Fix: GRANT SELECT ON macsync.messages, macsync.conversations TO quinn_icloud; # If returns 0 unexpectedly: MacSyncApp not mirroring. Check mac-sync-server logs. ``` The gate gracefully falls back to `[]` on query error โ€” see `quinn-outbound-query.ts`. ## State files (operator-facing) ``` ~/.local/share/quinn-outreach/ โ”œโ”€โ”€ engine-config.json Mode toggle. Edit via CLI only. โ””โ”€โ”€ block-list.json Wrong-identity registry. Edit via CLI only. ~/.config/systemd/user/ โ”œโ”€โ”€ quinn-outreach-watcher.service โ”œโ”€โ”€ quinn-outreach-parser.service โ”œโ”€โ”€ quinn-outreach-scheduled-send.service โ”œโ”€โ”€ quinn-outreach-booking.service โ””โ”€โ”€ quinn-outreach-autorespond.service ```