# Always Be Selling

**Agent-driven content engine for sequenced, research-grounded business outreach.**

23,243 leads in backlog — 8 renewal contracts — 77 sequenced — 21 sent to Zoho.

## Status

**MILESTONE 2 — Dedup, Sequences, Renewals, Import.** Full three-layer dedup (suppressions → Zoho → backlog), sequence eligibility gate with cooling periods and auto-substitution, contract renewal sequences, bulk CSV import, sequence trace written to Zoho.

## Architecture

```
CT SoS (Socrata) ──→ Harvest ──→ Score ──→ Enrich ──→ Research ──→ Draft ──→ Approve ──→ Push ──→ Zoho CRM
                                                                                                      │
                                                                                          Sequence Trace (custom fields)
                                                                                                      │
                         CSV Import ──→ Three-layer dedup ──→ Backlog ◄──── Zoho Renewal Scan
                         Zoho API   ──→ (suppressions → Zoho → backlog)                          
```

### Three-Layer Dedup

Every lead entry point (harvest, CSV import, renewal scan, manual create) passes through:

| Layer | Store | Mechanism |
|-------|-------|-----------|
| 1. Suppression | `data/suppressions.json` | Exact email + wildcard domain match. Auto-populated on opt-out burn. |
| 2. Zoho CRM | Zoho Leads & Contacts | Batch mode: pre-fetch all CRM emails into sets, O(1) lookup. Fails open. |
| 3. Backlog | `data/backlog.json` | Cross-file email dedup against existing leads. |

Skip Zoho layer with `--skip-zoho-check` flag on harvest/import/renewal commands.

### Data Sources

| Source | Dataset | Purpose |
|--------|---------|---------|
| CT SoS Business Master | `n7gp-d28j` | Core lead data — name, filing, NAICS, location |
| CT SoS Agents | `qh2m-n44y` | Registered agent — name, type |
| CT SoS Principals | `ka36-64k6` | Officers, directors — names, designations |
| CT SoS Filing History | `ah3s-bes7` | Filing activity signals |
| Zoho CRM Deals | API v8 | Contract renewal data |
| Zoho CRM Leads/Contacts | API v8 | Dedup reference |
| Brave Search | API | Website, Google Business data |
| Featherless LLM | API | Drafting, research extraction |

## Pipeline

### Harvest

Fetches CT SoS business filings. Three temporal categories:
- **Fresh** (0-14 days): Recent filings, discovery-based outreach
- **Recent** (15-89 days): Operating businesses
- **Historical** (90+ days): Equipment lifecycle framing, milestone-aware

Filters: entity type, citizenship, shell names, suppression registry.

### Score

Priority-based scoring (0-100): email domain tier, phone, NAICS tier, PLLC detection, location quality, recency bonus, filing history signals.

### Enrich

Agent info, principals, domain data, Brave search, competitor detection, equipment mapping. Filing history gated on score ≥ 70 or historical.

### Research

Website content + Google Business Profile extraction via Brave → LLM structured extraction → stores as `research_note`. Gated on live website or substantive Brave data.

### Draft

Principles-based prompt assembly (no scenario branching). One prompt base with temporal data injected as context — LLM decides framing. Key rules:
- Product anchor: physical copiers/printers only
- Subject: 4-8 words, must reference a specific detail about THIS business
- No em dashes (use -- instead)
- Phone mandatory in signature: `203-777-0011 x743`
- No bracket placeholders (`[Name]`, `[Company]` — three-layer defense)
- Close: one sentence for fresh leads, one differentiator fact for historical

### Sequence Engine

Multi-touch outreach across N touches / D days. Cold outreach uses front-loaded exponential schedule; renewals use deadline-anchored evenly distributed schedule.

**6 presets:**

| Preset | Touches | Days | Use For |
|--------|---------|------|---------|
| `light_45` | 3 | 45 | Thin data |
| `standard_60` | 4 | 60 | Default |
| `deep_90` | 6 | 90 | Researched leads |
| `extended_120` | 8 | 120 | High-value + competitor flag |
| `urgent_21` | 3 | 21 | Time-sensitive |
| `renewal_90` | 4 | 75 | Contract renewals |

**Sequence Eligibility Gate:**
- `_get_sequence_state()` classifies leads: eligible / active / paused / completed / burned / preview_only
- `check_sequence_eligibility()` — safety guard before any sequence operation
- Cooling periods between campaigns (default 120 days, per-preset overrides, renewal_90: no cooling)

**Batch with Auto-Substitution:**
- `sequence_batch()` generates sequences for eligible leads in parallel (max 6 workers)
- Blocked leads replaced with same-vertical/county substitutes via 4-pass matching

## Renewals

Contract renewal sequences sourced from Zoho Deals. Events trigger sequences — no cooling period.

### Workflow

```bash
# Scan Zoho for expiring contracts
absel renewals scan --days 120

# List renewal leads
absel renewals list [--urgent]

# Generate renewal sequences
absel renewals sequence --limit 20 [--approve]

# Manual creation
absel renewals create --email bob@acme.com --name "Acme Plumbing" \
  --model "AltaLink C8245" --expiration "2026-12-15"

# Due touches (renewal-only)
absel renewals due
```

### Upgrade Paths

20+ config-driven model mappings (e.g., AltaLink C8145 → C8245). Meter-based urgency: `recommended` (>50k pages) / `suggested` (>20k) / `optional`.

## CSV Import

```bash
absel import leads tradeshow.csv
absel import leads renewals.csv --type renewal --dry-run
absel import leads contacts.csv --intel "Q3 trade show"
absel import leads data.csv --mapping '{"Full Name":"contact_name","Org":"name"}'
```

Auto-detects column names from 13 known aliases (email, name, phone, city, state, zip, naics, equipment fields). Runs full three-layer dedup. Supports `--skip-zoho-check`.

## Reply Handling

5-category reply classifier:

| Category | Action |
|----------|--------|
| Autoresponder | Logged, sequence continues |
| Hard opt-out | Lead burned, sequence cancelled |
| Soft opt-out | Lead + sequence paused |
| Bounce | Email flagged, sequence paused (lead not burned) |
| Engagement | Sequence paused, operator drafts response |

Autoresponders do NOT pause sequences. Opt-out inside autoresponder → opt-out wins.

```bash
absel replies check          # Poll Zoho for new replies
absel replies list           # Show replied leads
absel replies list --unhandled
absel respond <id> "reply"   # Draft contextual response
```

## Zoho CRM Integration

### Push

```bash
absel push <id>              # Push single approved lead
absel push --batch           # Push all approved, un-pushed leads
absel push --batch --dry-run # Preview
```

Push uses a load-bearing two-call split: Call 1 sends all metadata + `Email_Draft_Subject`; Call 2 sends `Email_Draft_Body2`. This gap is required for Zoho email automation to fire reliably.

### Required Custom Fields

Create these in Zoho Leads module:

| Field | Type | Purpose |
|-------|------|---------|
| Email Draft Subject | Text | Draft subject line |
| Email Draft Body2 | Multi-line Text | Draft body |
| Lead Score | Number | Quality score (0-100) |
| Lead Category | Picklist | Fresh / Recent / Historical |
| Sequence | Text | Sequence trace — e.g. "4×75 — approved 2026-05-26" |
| Touch1–Touch6 | Multi-line (2000ch) | Per-touch reasoning + message |

Sequence Trace fields are auto-populated when a sequence is approved. Each touch field contains: purpose, differentiator, subject, and full message body.

### Reply Polling

`absel sequence due` calls `get_due_touches_safe()` which polls Zoho for replies BEFORE listing due touches. If Zoho poll fails, delivery is blocked (fail safe).

## CLI Reference

### Pipeline
| Command | Description |
|---------|-------------|
| `absel brief` | Harvest → score → enrich → draft → brief |
| `absel review` | Interactive paginated draft review |
| `absel counts` | Lead counts by status and stage |
| `absel health` | Check LLM model availability |

### Single Lead
| Command | Description |
|---------|-------------|
| `absel research <id>` | Generate research note |
| `absel draft <id>` | Draft a touch |
| `absel enrich <id>` | Enrich a lead |
| `absel status <id>` | Complete lead state |

### Harvest
| Command | Description |
|---------|-------------|
| `absel harvest recent` | Last N days |
| `absel harvest range` | Historical date range |
| `absel harvest milestone` | Near milestone anniversary |

### Sequences
| Command | Description |
|---------|-------------|
| `absel sequence preview <id>` | Preview sequence |
| `absel sequence approve <id>` | Approve and store |
| `absel sequence status <id>` | Show sequence state |
| `absel sequence resume <id>` | Resume paused |
| `absel sequence cancel <id>` | Cancel remaining |
| `absel sequence skip <id> N` | Skip specific touch |
| `absel sequence due` | List due touches |
| `absel sequence batch` | Auto-generate for eligible leads |

### Renewals
| Command | Description |
|---------|-------------|
| `absel renewals scan` | Scan Zoho Deals |
| `absel renewals list` | List renewal leads |
| `absel renewals create` | Manual creation |
| `absel renewals cancel` | Cancel renewal |
| `absel renewals sequence` | Generate renewal sequences |

### Import
| Command | Description |
|---------|-------------|
| `absel import leads <csv>` | Bulk CSV import |

### Outreach Control
| Command | Description |
|---------|-------------|
| `absel burn <id> [reason]` | Permanent removal |
| `absel pause <id> [reason]` | Pause touch clock |
| `absel unpause <id>` | Resume paused lead |
| `absel gate status\|close\|open` | Temporal gatekeeping |
| `absel purge --confirm` | Wipe entire backlog |

### Suppression Registry
| Command | Description |
|---------|-------------|
| `absel suppress list` | List all suppressions |
| `absel suppress add <email>` | Suppress email |
| `absel suppress add-domain <domain>` | Suppress domain |
| `absel suppress remove <entry>` | Remove suppression |
| `absel suppress check <email>` | Check suppression status |

### Competitive Intelligence
| Command | Description |
|---------|-------------|
| `absel intel agents` | Agent displacement |
| `absel intel principals` | Principal overlap |
| `absel intel addresses` | Address clustering |
| `absel intel wins` | Win-rate stub |
| `absel intel set/clear/show <id>` | Per-lead operator intel |
| `absel intel global [msg]` | Global operator intel |

### Export
| Command | Description |
|---------|-------------|
| `absel export outside-sales` | Leads without digital contact path |

### Reply Management
| Command | Description |
|---------|-------------|
| `absel replies check` | Poll Zoho for replies |
| `absel replies list` | Show replied leads |
| `absel respond <id> [text]` | Draft contextual response |

## Data Quality

- **Filing-service detection**: 20 known domains flagged as unusable. Leads with filing email + no phone marked for outside sales.
- **Signal tiers**: ★★★ Strong (email+phone+website), ★★ Good, ★ Basic, ⚠ Weak
- **Greeting resolution**: Deterministic chain — principal → agent → PLLC → company → none. Never extracts from email prefix.
- **Bracket placeholder defense**: Prompt rules → sequence auto-regen → push gate (three layers)
- **Phone validation**: `203-777-0011` required in every outgoing draft
- **Em dashes**: Banned in prompts and post-generation replacement

## Discipline Notes

- Agent phone/email NOT stored on leads — pending ethical review
- Principal residence addresses NOT stored — personal information exclusion
- Bulk queries use `$where=key IN (...)` — batch size 100 per request
- Filing History gated on score ≥ 70 or historical — avoids unnecessary API load
- Response drafts NEVER sent automatically — operator copies to Outlook manually
- Outside sales output is plain ASCII — no unicode, no ANSI, no markdown
- Sequence trace written to Zoho at approval time — if no zoho_id yet, pushed on next lead update

## Quick Start

```bash
cp .env.example .env  # Add API keys
pip install -r requirements.txt
python -m cli.main --help
```

## Testing

```bash
TEST_MODE=true python -m pytest tests/
```

## Safety

- `TEST_MODE=true` prevents external API calls
- `absel gate close` pauses outreach while harvesting continues
- Suppression registry auto-populated on opt-out burn
- Fail-open for Zoho dedup — API errors don't block lead creation
