SKILL.md
Do Not Contact
One suppression list, consulted by every outreach surface, so no agent or manual batch ever touches a person or company the owner has excluded. Partial coverage is the failure mode this closes: a list that guards only one pipeline stage guards nothing.
Files
All under $WORKDIR/pipeline/:
- Unified list (build artifact, never hand-edit):
exclusions/do-not-contact.jsonl - Build metadata (build time, source counts):
exclusions/do-not-contact.meta.json
Sources merged on every rebuild:
| Source | Contributes |
|---|---|
exclusions/disqualified-leads.jsonl | Company-scope and person-scope disqualifications |
manual-email-suppression.csv | Bounced and manually suppressed emails |
manual-lead-exclusions.csv | Friends, peers, bad leads, wrong profiles |
imports/buyer-exclusions-*.csv | Vendor, recruiter, and competitor exclusions |
outreach-log.jsonl | Records with status do_not_contact, bounced, excluded_bad_lead |
The Gate Contract
Maintain a rebuild script and a check script in the pipeline repo with this exact contract:
- Rebuild: full idempotent rebuild of the unified list from all sources, writing the meta file with a timestamp.
- Check: takes one candidate (email, LinkedIn URL, name, company) or a batch JSONL file. Prints a JSON verdict with
checked_at. Exit codes:0clear,1suppressed hit,2hard stop (list missing, unreadable, or older than the configured max age).
Matching rules the check must implement: email exact-match after lowercasing. LinkedIn URL match after stripping scheme, www., query strings, and trailing slashes. Company exact-match on normalized name against company-scope entries. Name-plus-company as the fallback person match when no email or URL exists. Any single identifier hit suppresses the candidate. Near-miss company aliases are not caught: review new discovery batches for aliases by hand.
Operating Rules
- Every outbound loop runs the live check twice: immediately before drafting and again immediately before send approval. A cached result is not a live check.
- Log the
checked_attimestamp into the loop's run record. - A hit means drop the candidate and record the reason. Never soft-suppress, never queue for later, never "reach out anyway just this once".
- Exit code
2halts the entire run: rebuild, re-check, log it as a gate incident. - New exclusions go into a source file (people into
manual-lead-exclusions.csv, companies intoexclusions/disqualified-leads.jsonl), then rebuild. Never append to the unified file directly; the next rebuild would erase it. - Rebuild at the start of any day that includes a send batch, so yesterday's bounces and opt-outs take effect today.
Verification
Run the check against a known-suppressed identifier from a source file. Expect exit code 1 and a verdict naming the source. Then run it against an obviously clear synthetic identifier. Expect exit code 0 with a fresh checked_at. If either result differs, the gate is broken: halt all outbound until the rebuild and check behave to contract.
Good vs Bad
Judgment call: a great-fit lead turns up in a new discovery batch, and the check returns a hit from an old bounce.
Bad: "The bounce was a year ago and they changed companies, I'll include them." The gate just became advisory, which means it no longer exists.
Good: Drop the candidate, record the hit reason, and surface it to the owner: "suppressed by 2024 bounce, flag if you want the source entry reviewed." Only a human edit to a source file plus a rebuild can clear someone.
Footguns
- Hand-editing the unified list. The next rebuild silently reverts your edit. Fix: edit sources, rebuild, verify the entry appears.
- Checking at discovery but not at send. Days pass between the two; yesterday's opt-out slips through. Fix: the check runs twice, draft-time and send-time, no exceptions.
- Treating exit code 2 as a skip. A missing list reads like "no hits" to sloppy code. Fix: hard stop semantics enforced by the calling loop, logged as an incident.
- Company aliases. "Acme Inc" on the list does not catch "Acme Corporation". Fix: manual alias review on every new discovery batch, add aliases to the source file.
Red Flags
Stop if you catch yourself thinking or saying:
- "The check passed this morning, that still counts"
- "It's just a follow-up, not a new touch"
- "They're clearly a good lead, the entry must be stale"
- "I'll add them back to the queue and check later"
- "The list is missing, so nobody's suppressed"
Every touch gets a live check, and only the owner clears an entry. There is no candidate valuable enough to bypass this gate; if one seems to be, that is the strongest sign the gate is doing its job.
Completion Checklist
- [ ] List rebuilt today if a send batch runs today
- [ ] Live check run before drafting AND before send approval
- [ ]
checked_atlogged in the run record - [ ] All hits dropped with reasons recorded, zero soft-suppressions
- [ ] Any exit code 2 halted the run and was logged as an incident
Any box unchecked: not done. Fix or say so.