# Email Pipeline + Edita PA — Runbook

# Email Pipeline + Edita PA — Runbook

**MC:** #8521 | **Related:** #8466 (OWN classifier fix) | **Date:** 2026-04-20

---

## Overview

The email pipeline classifies incoming emails and routes them to Mission Control, HiveMind, or archive. Edita PA is the autonomous email assistant operating in phased rollout (currently Phase 1).

### Architecture

- **Daemon:** `~/system/daemons/email-agent.js`
- **LaunchAgent:** `com.john.email-agent` (via wrapper `email-agent-wrapper.sh`)
- **Vault:** Bitwarden session (`/tmp/bw-session`) required for IMAP credentials
- **Triage LLM:** llama3.1:8b (Ollama ANVIL, preloaded via `ollama-triage-preload.sh`)

---

## OWN Classifier Logic

The OWN classifier identifies machine-generated emails from ALAI's own systems to prevent task spam.

### Constants (email-agent.js lines 118-123)

```
const OWN_SYSTEM_PREFIXES = [
  'noreply@', 'no-reply@', 'sentinel@', 'alerts@', 'auto@', 'daemon@',
  'mailer@', 'notification@', 'notifications@', 'bounces@', 'bounce@',
  'donotreply@', 'do-not-reply@', 'system@'
];
const OWN_SYSTEM_DOMAINS = ['@alai.no', '@basicconsulting.no'];

```

### isOwnSystemEmail() Function (lines 446-456)

Two-tier check:

1. **Exact match:** `OWN_ADDRESSES` array (hardcoded machine addresses)
2. **Prefix + domain:** Any prefix in `OWN_SYSTEM_PREFIXES` on domains in `OWN_SYSTEM_DOMAINS`

**Critical:** `alem@alai.no` is NEVER in this list. VIP check runs FIRST (line 464), bypassing OWN classifier entirely.

---

## TLDR\_SKIP Routing

Newsletters from `dan@tldrnewsletter.com` do NOT create MC tasks. They are handled exclusively by `tldr-briefing.js` daemon.

```
// line 126
const TLDR_SENDER = 'dan@tldrnewsletter.com';

// line 474
if (lowerFrom.includes(TLDR_SENDER)) {
  return { category: 'TLDR_SKIP', priority: 'low', summary: 'TLDR newsletter — handled by tldr-briefing.js', action: '' };
}

```

---

## VIP Ordering

Classification priority (lines 464-481):

1. **VIP:** CEO/family → bypass ALL filters, force ACTION/high, skip Ollama
2. **TLDR\_SKIP:** Newsletter → skip MC INTAKE, route to tldr-briefing.js
3. **OWN:** System emails → archive, no task
4. **Other:** Spam allowlist check → Ollama classification

---

## Edita PA Phases

### Phase 0: --dry-run (Log-Only)

Classification + logging only. No archive, no escalate, no respond.

```
node ~/system/daemons/email-agent.js --dry-run

```

### Phase 1: --allow-archive (CURRENT)

Archive low-priority emails only. Escalate and respond actions are held (logged but not executed).

```
node ~/system/daemons/email-agent.js --allow-archive

```

**Plist config:** `com.john.email-agent` calls `email-agent-wrapper.sh`, which passes no flags → defaults to Phase 1 (archive-only mode is internal default in daemon code).

### Phase 2: Full Live (NOT YET APPROVED)

Archive + escalate + respond. Requires CEO explicit approval.

```
node ~/system/daemons/email-agent.js --allow-all

```

---

## Unit Testing

Test classifier without IMAP/Vault dependencies:

```
node ~/system/daemons/test-email-classifier.js

```

**Scenarios (16 total):**

- VIP bypass (alem@alai.no, CEO family)
- TLDR\_SKIP routing
- OWN system emails (noreply@alai.no, sentinel@basicconsulting.no)
- Spam patterns with allowlist exceptions (GitHub, Cloudflare, Anthropic)

---

## Rollback

Revert to dry-run mode:

```
launchctl unload ~/Library/LaunchAgents/com.john.email-agent.plist

# Edit wrapper to add --dry-run flag
echo 'exec /opt/homebrew/bin/node "$HOME/system/daemons/email-agent.js" --dry-run' >> ~/system/tools/email-agent-wrapper.sh

launchctl load ~/Library/LaunchAgents/com.john.email-agent.plist

```

---

## Monitoring

- **Logs:** `~/system/logs/email-agent-launchd.log`
- **Errors:** `~/system/logs/email-agent-launchd-error.log`
- **MC tasks:** `node ~/system/tools/mc.js list --owner edita`
- **DLQ:** Failed vault sessions stored in `email-agent.js` in-memory DLQ (logged only, no persistence)

---

*Generated by Skillforge | ALAI, 2026*

---

## Contact Form Handlers

This section documents all contact forms across ALAI properties and their email delivery mechanisms.

### alai.no Contact Form

- **Frontend:** `https://alai.no/contact` (Cloudflare Pages)
- **Handler:** CF Pages Function `/functions/contact.js`
- **Endpoint:** `POST https://alai.no/api/contact`
- **Email provider:** Resend API
- **Recipient:** `info@alai.no`
- **Credentials:** Bitwarden item "Resend API Key" → CF Pages env var `RESEND_API_KEY`
- **Status:** LIVE (deployed 2026-04-21, MC #8587)

**Test procedure:**

```
curl -X POST https://alai.no/api/contact \
  -H "Content-Type: application/json" \
  -d '{"name": "Test User", "email": "test@example.com", "message": "E2E test 2026-04-21 14:00"}'

# Verify inbox:
himalaya search --account info-alai --folder INBOX "subject:Contact Form"

```

### snowit.ba Contact Form

- **Frontend:** `https://snowit.ba/contact`
- **Handler:** BROKEN — Vercel API route not migrated to CF Pages (MC #8591)
- **Endpoint:** `POST https://api.basicconsulting.no/contact` (hijacked by documenso-webhook, returns false success)
- **Recipient:** `info@snowit.ba` (LumisCare side, not ALAI-managed)
- **Status:** BROKEN — awaiting CodeCraft fix

### getdrop.no Waitlist

- **Frontend:** `https://getdrop.no` (Cloudflare Pages)
- **Handler:** CF Pages Function `/functions/waitlist.js`
- **Endpoint:** `POST https://getdrop.no/api/waitlist`
- **Storage:** Cloudflare D1 database `drop-waitlist`
- **Email provider:** None (DB-only storage, no email sent)
- **Status:** LIVE

**Test procedure:**

```
wrangler d1 execute drop-waitlist --command "SELECT * FROM submissions ORDER BY created_at DESC LIMIT 5"

```

### merdzanovic.ba Contact Form

- **Status:** UNKNOWN — needs audit (likely same risk as snowit.ba)
- **MC Task:** #8593 (audit all ALAI-managed contact forms)

---

## Form Handler Migration Checklist

When migrating sites from Vercel/Netlify to Cloudflare Pages:

1. **Inventory:** Identify all POST endpoints (forms, webhooks, API routes)
2. **Port handlers:** Rewrite Vercel API routes as CF Pages Functions (`/functions/*.js`)
3. **Environment variables:** Copy SMTP/API credentials to CF Pages env vars
4. **Update form actions:** Change form targets to new CF Pages routes (e.g., `/api/contact`)
5. **E2E test:** Follow [Forms E2E Testing Protocol](https://docs.basicconsulting.no/books/testing-qa/page/forms-e2e-testing-protocol) (HTTP + inbox check MANDATORY)
6. **Monitor:** Check inbox/DB for 24 hours post-migration to catch silent failures

**Reference incident:** [2026-04-21 alai.no Contact Form Failure](https://docs.basicconsulting.no/books/operations/page/incident-2026-04-21-alai-no-contact-form-failure)

---

## Himalaya IMAP Setup (for Form Testing)

Himalaya CLI provides rapid inbox verification without browser login.

### Install

```
brew install himalaya

```

### Configure Account

Add to `~/.config/himalaya/config.toml`:

```
[accounts.info-alai]
default = false
email = "info@alai.no"
display-name = "ALAI Info"

[accounts.info-alai.imap]
host = "imap.one.com"
port = 993
encryption = "tls"
login = "info@alai.no"
passwd.cmd = "bw get password 'Email - info@alai.no' --session $(cat /tmp/bw-session)"

[accounts.info-alai.smtp]
host = "send.one.com"
port = 587
encryption = "start-tls"
login = "info@alai.no"
passwd.cmd = "bw get password 'Email - info@alai.no' --session $(cat /tmp/bw-session)"

```

### Usage

```
# Unlock Bitwarden first
bw unlock --raw > /tmp/bw-session

# List recent messages
himalaya list --account info-alai --folder INBOX --page-size 20

# Search for form submissions
himalaya search --account info-alai --folder INBOX "from:noreply@alai.no"

# Search by date range
himalaya search --account info-alai --folder INBOX "since:2026-04-21"

```

**Credentials:** Bitwarden item "Email - info@alai.no"

---

*Updated: 2026-04-21 | Skillforge*