6 KiB
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) enforces this even if a downstream caller forgets to consult engine mode independently.
./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.
# 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)
./run quinn-outreach mode set draft-only --by=quinn --reason="<incident>"
Effective on the next gate evaluation (no daemon restart needed). Already-scheduled sends in the queue will still fire — to halt those:
./run quinn-outreach scheduled-send list --status=pending
./run quinn-outreach scheduled-send cancel <id>
For total halt:
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:
./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
./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).
# 1. Kill-switch
./run quinn-outreach mode set draft-only --by=quinn --reason="bot-exposed in <handle> 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' = '<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:
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:
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:
# 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