# FIRST: Load Working Memory

Before doing ANYTHING else in a new session, read the file at:
/root/.openclaw-abs/state.md

This is your working memory. It contains:
- What work is currently in progress
- Pipeline counts and statuses
- Pending tasks and blockers
- Critical reminders you keep forgetting

Read it. Internalize it. Then proceed with the operator's request.

At the END of every session (or when significant work completes),
update state.md with the current status. Keep it under 50 lines.
If you can't update it (session ending abruptly), that's fine —
the operator may update it manually.

If state.md doesn't exist, create it from what you know.

---

# SKILL.md — nuclearscalpel platform tools

> Reference for the abs agent. Know what each tool does, where it
> lives, and how to use it. When the operator asks you to do
> something, find the right tool here and use the CLI or function
> directly — don't reinvent what's already built.

---

## System Map

```
/root/alwaysbeselling/   ABS — outreach engine (generates + sends)
/root/zohar/             ZOHAR — signal harness (reads + surfaces CRM)
/root/sellsat/           SELLSAT — business intelligence (knows + analyzes)
/root/shared/            SHARED — auth, event bus, identity layer
```

Each system has its own venv. Activate before running:
```bash
cd /root/alwaysbeselling && source .venv/bin/activate
cd /root/zohar && source .venv/bin/activate
cd /root/sellsat && source .venv/bin/activate
```

---

## ABS — alwaysbeselling

**CLI prefix:** `python -m cli.main` (from `/root/alwaysbeselling/`)

### Daily Agenda

Run these in order each morning:

```bash
absel brief                              # Full pipeline: score + enrich + morning report
absel sequence due                       # Touches due today (cold + renewal)
absel renewals sequence --batch          # Sequence any unsequenced renewal leads
absel replies check                      # Poll for new replies
absel renewals scan --dry-run            # Check for new expiring contracts
```

**Renewal-specific daily checks:**

```bash
absel renewals due                       # Renewal touches due today
absel renewals due --push                # Push due renewal touches to Zoho
absel renewals list --urgent             # Renewals expiring within 30 days
absel renewals list                      # All renewal leads, expired first
```

Expired contracts (⚠ flagged) bypass the standard cold sequence and use
`renewal_lapsed` preset (3 touches / 21 days, warm service-recovery tone).
Classified automatically — no manual flagging needed.

### Harvest (find new leads)

```bash
absel harvest --days 7                    # Recent filings (last 7 days)
absel harvest --from 2021-01-01 --to 2021-12-31  # Historical range
absel harvest --milestones 5             # 5-year anniversary leads
```

### Score + Enrich

```bash
absel brief                              # Run full pipeline: score + enrich + morning report
absel enrich <lead_id>                   # Enrich a single lead
```

The morning brief now includes three additional sections:
- **Stale Sequences** — approved touches past their scheduled date that weren't sent, with days overdue
- **Reply Backlog** — unhandled replies sorted by age, with ⚠ at 2+ days
- **Campaign Health** — aggregate stats: active sequences, touches sent/total, reply rate, opt-out rate, autoresponder count, bounces

These sections appear automatically when there's data to show. No separate command needed — they're part of the standard `absel brief` output.

### Enrichment Gate (mandatory)

No lead can be drafted or sequenced without enrichment_status = "enriched".
If you try to draft or sequence an un-enriched lead, the system rejects it:

    "Lead X has not been enriched. Run 'absel enrich <id>' before drafting."

Always enrich before drafting. Always. The enrichment gate is enforced
in code — you cannot bypass it.

### Enrichment Phase Order

Phase 1: Domain check (website exists?)
Phase 2: NAICS mapping (equipment context)
Phase 3: Brave Search (business descriptions, phone, county, brave_website)
Phase 3.5: Prospeo contact enrichment (verified email + name)
  - Fires when: principal_name exists AND email not yet verified
  - Runs after Brave so brave_website is available (never use `domain`
    for the lookup — it can be a filing-service domain like incfile.com)
  - Input: principal name + brave_website
  - Output: verified email, greeting name
  - Cost: 1 credit per hit, misses are free
  - Hit rate on small CT businesses: ~31% (4/13), resolves generic
    info@ addresses to principal's personal email
Phase 4: Competitor detection
Phase 5: Agent/principal from SoS
Phase 6: Filing history

### Research

```bash
absel research <lead_id>                 # Deep research (website + Google Business)
absel research <lead_id> --force         # Re-research (overwrite existing)
absel research --batch --limit 20        # Batch research top 20 researchable leads
absel research --batch --dry-run         # Preview what would be researched
```

### Draft

```bash
absel draft <lead_id>                    # Draft a single email
absel draft --batch --limit 20           # Batch draft
```

### Sequence (multi-touch campaigns)

```bash
absel sequence preview <lead_id>                    # Auto-select preset, show preview
absel sequence preview <lead_id> --sequence deep_90  # Specific preset
absel sequence approve <lead_id>                    # Approve previewed sequence
absel sequence approve <lead_id> --touches 4 --days 60  # Override envelope
absel sequence batch --county Fairfield --limit 66  # Batch: auto-select per lead
absel sequence batch --county Fairfield --workers 6 --dry-run  # Preview batch
absel sequence status <lead_id>                     # Check sequence state
absel sequence due                                  # Touches due today
absel sequence due --push                           # Push due touches to Zoho
absel sequence resume <lead_id>                     # Resume paused sequence
absel sequence cancel <lead_id>                     # Cancel remaining touches
```

**Presets:** light_45 (3t/45d), standard_60 (4t/60d), deep_90 (6t/90d),
extended_120 (8t/120d), urgent_21 (3t/21d aggressive), renewal_90 (4t/75d)

**Auto-selection:** If no preset specified, `recommend_sequence()` picks
based on data richness + temporal category + competitor flag.

### Send Throttle

```bash
absel sequence due --push                # Throttled by default (weekly spread)
absel sequence due --push --limit 5      # Override: send exactly 5 today
absel sequence due --push --no-throttle  # Send everything (confirmation required)
absel sequence set-throttle <id> --off   # Exempt a sequence from throttle
absel sequence set-throttle <id> --on    # Re-enable throttle on a sequence
```

Default weekly spread: Mon 25%, Tue 25%, Wed 20%, Thu 20%, Fri 10%, Sat/Sun 0%.
Max 15 per day. Sorted by score descending — best leads send first.

Cold outreach: throttled by default.
Renewals: unthrottled by default (time-sensitive).
Urgent_21: unthrottled by default.

The daily push splits into two pools:
  IMMEDIATE: unthrottled touches (renewals, urgent) — all send
  THROTTLED: cold touches — obey the weekly spread

### Renewal (contract renewal campaigns)

```bash
absel renewals scan --days 120           # Pull upcoming renewals from Zohar
absel renewals list                      # Show renewal leads in backlog
absel renewals list --urgent             # Within 30 days only
absel renewals create \                  # Manual creation (3 required fields)
  --email amy@starline.com \
  --model "AltaLink C8145" \
  --expiration "7/27/2026" \
  --contact "Amy Johnson" \              # Optional — extracted from email if omitted
  --name "Starline Graphics" \           # Optional — extracted from domain if omitted
  --serial "EHQ555555" \                 # Optional
  --mono 45000 --color 12000 \           # Optional — improves upgrade recommendation
  --phone "203-555-1234"                 # Optional
absel renewals sequence <lead_id>        # Preview renewal sequence
absel renewals sequence --batch          # Batch all renewal leads
absel renewals due                       # Renewal touches due today
absel renewals due --push                # Push due renewal touches to Zoho
```

### Review

```bash
absel review                             # Interactive review queue
absel review --strong-only               # High-signal leads only
absel review --drafted-only              # Only leads with drafts
```

In the review queue: `a` approve, `b` burn, `p` pause, `d` delete,
`r` regen, `n` next, `q` quit.

### Push to Zoho (two paths, same two-call split)

```bash
absel push <lead_id>                     # Push single approved lead
absel push --batch                       # Push all approved leads
absel push --dry-run                     # Preview without pushing
```

The push routes on lead_type automatically:

**Cold leads (lead_type = "cold"):**
  Call 1: All fields + Sequence + Touch_1 through Touch_6
  Call 2: Email_Draft_Body2 (send trigger — fires workflow)
  Lead_Source: "CT SoS"

**Renewal leads (lead_type = "renewal"):**
  Call 1: All fields + Lease_End_Date, Digital_Copiers_Type,
          Category, Page_Band, Package_Size, Operator_Intel,
          Renewal_Trace
  Call 2: Current_Touch_Body (send trigger — fires workflow)
  Lead_Source: "Renewal"

Both paths use the two-call split. Never consolidate into one call.
The operator does not need to specify which path — push_lead_to_zoho()
reads lead_type and routes automatically.

### Reply Handling

```bash
absel respond <lead_id> "reply text"     # Draft response to a reply
absel replies check                      # Poll Zoho for new replies
absel replies list --unhandled           # Unhandled replies
```

Reply classification: autoresponder (continue), hard opt-out (burn),
soft opt-out (pause), bounce (flag), engagement (pause for response).

### Suppressions (global opt-out registry)

```bash
absel suppress list                    # Show all suppressed emails/domains
absel suppress add email@domain.com    # Suppress an email
absel suppress add-domain badco.com    # Suppress entire domain
absel suppress remove email@domain.com # Remove suppression
absel suppress check email@domain.com  # Check if suppressed
```

Suppressions are checked at 4 gates: harvest dedup, renewal scan, manual
create, and Zoho push. When a lead is burned via hard opt-out, their email
is automatically added to suppressions. Future harvests with the same email
are filtered out even if they appear on a different business filing. This is
a compliance layer — it prevents re-emailing contacts who opted out.

### CSV Import (bulk lead ingestion)

```bash
absel import leads contacts.csv                        # Auto-map common columns
absel import leads contacts.csv --dry-run               # Preview without importing
absel import leads contacts.csv --type renewal          # Import as renewal leads
absel import leads contacts.csv --intel "Trade show Q3"  # Apply batch intel
absel import leads contacts.csv --mapping email=Email,name=Company  # Custom column mapping
```

Auto-detects common column names (Email, Company, Contact, Phone, City,
Model, Serial). Checks each row against the suppression registry and
existing backlog for deduplication. Imported leads are raw — they still
need enrichment, research, drafting, and sequencing. The output tells
you the next step.

### Competitive Intelligence

```bash
absel intel agents                       # Agent displacement rates
absel intel principals                   # Principal overlap
absel intel addresses                    # Address clustering
```

### Outside Sales

```bash
absel export outside-sales --county "New Haven"   # ASCII list for Brian
```

### Health + Status

```bash
absel health                             # Check all 4 LLM models
absel gate status                        # Check push gate (open/closed)
absel gate open                          # Open the gate
absel gate close --reason "testing"      # Close the gate
```

### Backfill

```bash
absel backfill-ids --top 100 --enrich    # Backfill business IDs on top leads
```

---

## ZOHAR — Signal Harness

**CLI prefix:** `zohar` (from `/root/zohar/`) or `python -m zohar.cli.main`

### Deals

```bash
zohar deal search <query>                # Search deals + leads
zohar deal get <id>                      # Full record + timeline
zohar deal pending                       # Contracts in "Contract Sent"
zohar deal leads                         # All leads
zohar deal stale --days 7                # Stale deals
```

### Tasks

```bash
zohar tasks open                         # All open tasks
zohar tasks open --owner Barb            # Filter by owner
zohar tasks overdue                      # Overdue tasks
zohar tasks today                        # Due today
zohar tasks summary                      # Aggregate by owner
```

### Equipment

```bash
zohar equipment search <query>           # Search by name/serial/account
zohar equipment available                # What's in stock
zohar equipment available --model AltaLink  # Filter by model family
zohar equipment for <account>            # What does a customer have
zohar equipment serial <serial>          # Lookup by serial number
zohar equipment summary                  # Inventory overview
```

### Emails

```bash
zohar emails for <record_id>             # Email history for a record
zohar emails replies --days 7            # Recent replies
zohar emails opens --days 7              # Recent opens
zohar emails bounces --days 7            # Recent bounces
zohar emails summary --days 7            # Aggregate stats
```

Note: Emails module requires OAuth scope `ZohoCRM.modules.emails.ALL`.
Functions degrade gracefully if scope isn't granted.

### Renewals

```bash
zohar renewals upcoming --days 120       # Contracts expiring within N days
zohar renewals summary                   # Count by time window
zohar renewals account <name>            # Specific account renewal details
```

### Alerts + Polling

```bash
zohar poll                               # Force a poll now
zohar alerts                             # Recent alerts
zohar alerts --today                     # Today's alerts only
zohar alerts --since "2026-05-24"        # Since a date
```

Polling cron runs every 15 minutes automatically:
`*/15 * * * * /root/zohar/.venv/bin/python3 -m zohar.polling.alerter`

### Morning Brief

```bash
zohar brief                              # Full morning brief
```

Returns: deals, tasks (overdue + due today), equipment changes,
renewals, outreach touches due, replies, alerts.

### MCP Tools (available to you directly)

These tools are registered in your OpenClaw profile. Call them
directly — no CLI needed:

| Tool | What to ask |
|------|-------------|
| `get_deal_status` | "What's happening with Fairfield Dental?" |
| `get_pending_actions` | "What needs my attention?" |
| `get_morning_brief` | "Give me the full picture" |
| `get_tasks_status` | "Is Barb caught up?" |
| `get_equipment_status` | "Do we have a C8245 in stock?" |
| `get_alerts` | "What changed since I last checked?" |

---

## SELLSAT — Business Intelligence

**CLI prefix:** `sellsat` (from `/root/sellsat/`) or `python -m sellsat.cli.main`

### Harvest

```bash
sellsat harvest                          # Full harvest (all 4 datasets, ~28 min)
sellsat harvest --dataset businesses     # Single dataset
sellsat harvest --status                 # Show progress
sellsat harvest --resume                 # Resume interrupted harvest
```

### Precompute

```bash
sellsat compute                          # Run all precomputes (~3 min)
sellsat compute --agents                 # Agent profiles only
sellsat compute --principals             # Principal networks only
sellsat compute --counties               # County trends only
sellsat compute --matrix                 # Industry-agent matrix only
sellsat compute --status                 # Last compute timestamps
```

### Agents (formation agent intelligence)

```bash
sellsat agents top                       # Top 10 by portfolio quality
sellsat agents top --limit 25            # Top 25
sellsat agents top --county Fairfield    # Filter by county
sellsat agents profile "Reid and Riege"  # Full portfolio profile
sellsat agents vertical 621              # Who files for healthcare (NAICS 621)
sellsat agents vertical 621 --county "New Haven"  # Healthcare agents in New Haven
sellsat agents partnership              # Partnership candidates (local firms, filtered)
sellsat agents partnership --min-entities 30  # Lower entity threshold
sellsat agents partnership --county Fairfield  # Filter by county
sellsat agents partnership --include-national  # Include national agents (excluded by default)
```

### Principals (serial entrepreneurs)

```bash
sellsat principals                       # Top principals by entity count
sellsat principals --min-entities 5      # Only those with 5+ entities
sellsat principals "John Smith"          # Specific person's network
```

### County Trends

```bash
sellsat county "Fairfield"               # Fairfield formation trends
sellsat county "Fairfield" --years 3     # Last 3 years only
sellsat county all                       # All counties compared
```

### Entity Lookup

```bash
sellsat lookup "Sas Strategic"           # Deep dive on one business
```

### Status

```bash
sellsat status                           # DB stats: records, harvest date, disk usage
```

### MCP Tool (available to you directly)

One tool registered in your OpenClaw profile:

| Tool | What to ask |
|------|-------------|
| `query_intelligence` | "Top agents," "tell me about Reid and Riege," "who files for dental in Fairfield," "principals with 5+ entities," "what's forming in New Haven," "partnership candidates," "who should I call," "referral targets," "agents to call" |

### Query Functions (what the MCP tool routes to)

| Function | What it answers | When to use |
|----------|----------------|-------------|
| `agent_ranking()` | Top formation agents by portfolio quality | "top agents," "partnership targets," "best agents" |
| `agent_profile(name)` | Full portfolio for one agent | "tell me about Reid and Riege," "profile for [name]" |
| `agents_by_vertical(naics, county)` | Which agents dominate a vertical | "who files for dental in Fairfield" |
| `principal_network(name)` | Serial entrepreneurs, multi-entity principals | "principals with 5+," "who owns [name]" |
| `county_trends(county, years)` | Formation trends by county | "what's forming in New Haven," "trends in Fairfield" |
| `entity_lookup(name)` | Deep dive on one business | "look up Sas Strategic," "find Splash Car Wash" |

If a query doesn't map to any of these, **SQL fallback is expected
and legitimate.** Drop to raw SQL against `/root/sellsat/data/ct.db`.
Don't apologize for it — just run the query. See the schema below.

### Sellsat Database Schema

The database is a local SQLite file at `/root/sellsat/data/ct.db`
(~2.5 GB, 14.3M records). Four raw tables plus precomputed views.

```sql
-- Raw tables (from CT Secretary of State Socrata API)

businesses (
    id TEXT PRIMARY KEY,        -- business key (joins to all other tables)
    name TEXT,                  -- business name
    entity_type TEXT,           -- LLC, Stock, LP, etc. (PLLC stored as LLC — check name)
    status TEXT,                -- Active, Dissolved, Withdrawn, Expired Reservation
    naics TEXT,                 -- NAICS code (e.g., "621111")
    naics_description TEXT,     -- NAICS description (e.g., "Offices of Physicians")
    filing_date TEXT,           -- formation date
    dissolution_date TEXT,
    citizenship TEXT,           -- Domestic, Foreign
    city TEXT,                  -- town name
    state TEXT,
    county TEXT,                -- populated via city→county mapping (8 CT counties)
    annual_report_due TEXT,
    woman_owned INTEGER,        -- 0/1
    veteran_owned INTEGER,      -- 0/1
    minority_owned INTEGER,     -- 0/1
    harvested_at TEXT
)

agents (
    id INTEGER PRIMARY KEY,
    business_key TEXT,          -- FK → businesses.id
    agent_name TEXT,
    agent_type TEXT,            -- Individual, Business, Secretary of State
    agent_address TEXT,
    agent_city TEXT,
    agent_state TEXT,
    agent_zip TEXT
)

principals (
    id INTEGER PRIMARY KEY,
    business_key TEXT,          -- FK → businesses.id
    principal_name TEXT,        -- ⚠ NOT NORMALIZED — see warning below
    designation TEXT,           -- President, CEO, Managing Member, Officer, etc.
    principal_type TEXT,        -- Individual, Business
    city TEXT,
    state TEXT
)

filings (
    id INTEGER PRIMARY KEY,
    business_key TEXT,          -- FK → businesses.id
    filing_date TEXT,
    filing_type TEXT,           -- Formation, Amendment, Annual Report, etc.
    filing_category TEXT,
    report_year TEXT,
    organizer_name TEXT,
    physical_filing INTEGER     -- 0/1 (paper vs electronic)
)
```

Join key for everything: `businesses.id = agents.business_key =
principals.business_key = filings.business_key`

Precomputed tables: `agent_profiles`, `principal_networks`,
`county_trends`, `agent_industry_matrix`. Query these for fast
answers. Use raw tables only for entity lookups and ad-hoc queries.

### ⚠ CRITICAL: Principal Name Normalization

**`principal_name` in the raw `principals` table is NOT deduplicated.**
The CT Secretary of State stores names in inconsistent casing across
filings. The same person appears as multiple strings:

```
"JAMES FORD"   — on SPLASH CAR WASH, INC. (designation: Officer)
"James Ford"   — on 83 other Splash entities (no designation)
```

Without normalization, a GROUP BY on `principal_name` fragments
Ford into multiple nodes. His 84-entity network appears as several
smaller fragments, none showing the true scale.

**RULE: ALWAYS use `UPPER(TRIM(principal_name))` when grouping or
matching principals in ad-hoc SQL.** The precomputed `principal_networks`
table handles this automatically (grouped on normalized key). But
any direct query against the raw `principals` table MUST normalize
or it will undercount.

```sql
-- WRONG (fragments principals by case):
SELECT principal_name, COUNT(*) FROM principals
GROUP BY principal_name;

-- RIGHT (merges case variants):
SELECT UPPER(TRIM(principal_name)) AS principal, COUNT(DISTINCT business_key)
FROM principals
GROUP BY UPPER(TRIM(principal_name));
```

### PLLC Detection

CT Secretary of State stores ALL LLCs and PLLCs with `entity_type = 'LLC'`.
The PLLC distinction is only in the business name. To find PLLCs:

```sql
WHERE name LIKE '%PLLC%' OR name LIKE '%P.L.L.C%'
```

Do NOT rely on `entity_type = 'PLLC'` — it doesn't exist in this data.

### Knowledge Base

Business context is loaded from `/root/knowledge/abs-xerox/`:

- `company.yaml` — identity, territory, team, revenue model
- `products.yaml` — full product catalog with segments and monthly values
- `verticals.yaml` — equipment needs per vertical (volume, key feature, typical setup, monthly)
- `partnership.yaml` — ideal agent profile, red flags, national agent exclusion list, pitch template
- `service-contracts.yaml` — service contract framing rules (NEVER say "included" — say "with a service contract")

The knowledge base path is configurable via `KNOWLEDGE_BASE_PATH` env var.
Default: `/root/knowledge/abs-xerox/`

### Worked Example: Scoping a Business Network

Operator asks: "Scope the Splash Car Wash network"

This doesn't map to a named function — it's an ad-hoc brand/network
query. Use SQL fallback:

```sql
-- Step 1: Find all entities matching the brand
SELECT name, status, city, county, filing_date
FROM businesses
WHERE name LIKE '%Splash Car Wash%'
ORDER BY filing_date DESC;

-- Step 2: Find who's behind them (NORMALIZED)
SELECT UPPER(TRIM(p.principal_name)) AS principal,
       COUNT(DISTINCT b.id) AS entities
FROM principals p
JOIN businesses b ON p.business_key = b.id
WHERE b.name LIKE '%Splash Car Wash%'
GROUP BY UPPER(TRIM(p.principal_name))
ORDER BY entities DESC;
-- Result: JAMES FORD — 84 entities, DANIEL PETRELLE — 1

-- Step 3: See Ford's full network (beyond just Splash)
SELECT b.name, b.status, b.naics_description, b.city
FROM principals p
JOIN businesses b ON p.business_key = b.id
WHERE UPPER(TRIM(p.principal_name)) = 'JAMES FORD'
ORDER BY b.filing_date DESC;
```

The SQL fallback is not a failure — it's the tool working as designed
for queries that don't fit the named functions. The named functions
handle the common patterns (top agents, principal networks, county
trends). Everything else is a SQL query away.

---

## SHARED — Infrastructure

### Zoho Auth

Both ABS and Zohar use the shared auth layer at `/root/shared/shared/zoho/`.
Credentials in each system's `.env` file. Never expose credentials
in output, documentation, or logs. Reference as `[REDACTED]`.

### Event Bus

SQLite at `/root/shared/events.db`. Systems publish and subscribe
through events — never import from each other.

```python
from shared.eventbus import publish_event, get_events

# Publish
publish_event("alwaysbeselling", "email_sent", entity_id="...",
              entity_name="...", payload={...})

# Query
events = get_events(entity_id="lead_123", since="2026-05-24")
```

### Identity Layer

SQLite at `/root/shared/identity.db`. Maps entities across systems.

```python
from shared.identity import register_entity, find_entity

# Register (when pushing lead to Zoho)
register_entity(abs_lead_id="...", zoho_lead_id="...",
                email="...", business_name="...")

# Lookup (cross-system matching)
entity = find_entity(email="amy@starline.com")
```

---

## Key Rules

### Never-say list (enforced in code + prompt)
"We rent, we don't lease" · "Congratulations"/"Congrats" ·
"No pressure" · "I'm here as a resource" · "Feel free to reach out" ·
"I read the state filing" · "I can outline" · "Let's discuss" ·
"When you're ready" · "Happy to chat" · "We discussed" ·
Any competitor brand (Toshiba, Ricoh, Canon, HP, Brother, Sharp,
Konica Minolta, Kyocera, Lexmark) · "WorkCentre" (EOL)

### Credential safety
Never include API keys, OAuth tokens, client secrets, or refresh
tokens in any output, documentation, report, or log. Always use
`[REDACTED]` or reference the env var name.

### Zoho field names
| Field | Purpose |
|-------|---------|
| Email_Draft_Body2 | Draft body (NOT Body1 — that's gardener) |
| LLMResearch_Note | Research note (Rich Text, plain formatting) |
| LLMAnchor | Equipment context / draft anchor |
| Business_Vertical | NAICS vertical derivation |
| Vertical_Segment | NAICS sub-segment |

### LLM fallback chain
Primary: DeepSeek V3.1 → Fallback 1: Qwen3.6-27B → Fallback 2:
Gemma-4-31B-it → Fallback 3: Mistral-7B. 3 passes × 4 models.
"At capacity" falls through immediately. Check with `absel health`.

### Approval gate
Nothing goes to a prospect without `reviewed_at` set. The gate is
enforced in code, not discipline. `push_lead_to_zoho()` checks it.

### Product guidance
Recommended: AltaLink C8230/C8235/C8245/C8255/C8270, VersaLink B415/B7025.
Restricted: WorkCentre (any model) — EOL, support contracts only.
Low-volume: VersaLink C605 — not for daily print volume.
Rule: never invent model numbers.

### Filing-service emails
Never sequence or push leads with filing-service emails (@incfile.com,
@legalzoom.com, @nwregisteredagent.com, etc.). Route to outside sales.

### Suppression safety
Burned leads' emails are automatically suppressed globally. Future
harvests, renewal scans, and manual creates all check the suppression
registry before creating leads. The push gate checks suppressions as
a final safety check before Zoho delivery. Never manually bypass the
suppression check.

### Service contract framing
Toner, parts, labor, and staples are covered by a SERVICE CONTRACT —
a separate agreement from the lease or rental. NEVER say "we include
all toner" or "toner and parts are included." ALWAYS say "with a
service contract, toner, parts, and maintenance are covered." This
applies to drafts, pitches, partnership reports, and any prospect-
facing language.

### Partnership agent evaluation
When asked about partnership targets, use `sellsat agents partnership`
which filters by the ideal profile in partnership.yaml (50-500 entities,
>20% tier-1, local CT firms, excludes national filing services). Do NOT
recommend national agents like Northwest, Registered Agents Inc, or CSC
as partnership targets — their clients have no relationship with them.
Their tier-1 clients ARE valid as direct outreach leads, just not
partnership targets.

### Null-safe field access
Always use `(lead.get("field") or {}).get("subfield")` for nested
fields from JSON data. Python's `.get("key", {})` returns `None`
(not `{}`) when the key exists with value `None`.

### Enrichment is mandatory
The pipeline order is: harvest → enrich → draft → sequence → approve → push.
Enrichment sets enrichment_status = "enriched" on the lead. Drafting and
sequencing check for this status and reject leads without it. If a lead
fails to draft with "not enriched" error, run absel enrich <id> first.
Never skip enrichment — it provides the greeting name, agent data,
competitor detection, and research context that make the email specific
instead of generic.

### Large output workaround
When tool output exceeds ~50 lines, write to `/tmp/` as JSON and
read the file instead of relying on stdout. OpenClaw's session
history can drop large tool results. File-based output is reliable.

### Greeting resolution order
1. Principal name (from SoS/Sellsat) — best source
2. Contact name (from operator input or Zoho)
3. Email prefix (if it looks like a name, >= 3 chars, not in generic list)
4. Company name (last resort)

Generic prefixes that are NOT used as names: info, admin, office,
contact, hello, team, support, billing, sales, service, noreply,
accounting, hr, mail, help, general, reception, treasurer, secretary,
webmaster, postmaster, ops, management, director, manager, exec.

When email prefix is used, greeting_name_source = "email_prefix".

### Large output workaround
When tool output exceeds ~50 lines, write to `/tmp/` as JSON and
read the file instead of relying on stdout. OpenClaw's session
history can drop large tool results. File-based output is reliable.

```bash
cd /root/alwaysbeselling && source .venv/bin/activate && python3 -c "
from capabilities.sequencing import ...
result = ...
import json
with open('/tmp/result.json', 'w') as f:
    json.dump(result, f, indent=2)
print('Written to /tmp/result.json')
"
```
