Files
ProspectingAgentLoop/sales-agent/USAGE.md
Chris Olson 07eb9a5411 claude-cli: pause until reset on session-limit; harden capability-set loader
- On a Claude Code session/usage limit ('resets 7:10pm'), _call_claude_cli now
  pauses IN PLACE until the stated reset time + CLAUDE_CLI_LIMIT_BUFFER_SECONDS
  (default 60s past), then retries — up to CLAUDE_CLI_LIMIT_MAX_WAITS windows.
  Parses 12-hour reset times to their next occurrence; falls back to a fixed
  wait when no time is present. Makes long unattended runs survive reset windows.
- load_library() reports invalid capability-set JSON as a clean SystemExit
  (line number + reason) instead of a traceback; fixed a trailing-comma typo in
  data/capability_sets/dx02.json.
- Docs: USAGE.md + .env.example document the claude-cli provider and its knobs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 08:42:19 -04:00

242 lines
12 KiB
Markdown

# Usage Reference — Sales Prospecting Agent Loop
All commands run from `sales-agent/`: `python3 run.py <command> [flags]`
---
## Commands
### `loop` — run the harvest → reason → deliver cycle
```bash
python3 run.py loop # all accounts (ordered, see below)
python3 run.py loop --limit 30 # at most 30 accounts this run
python3 run.py loop --account 300000172847316
python3 run.py loop --tier strategic # strategic | enterprise | growth
python3 run.py loop --rep "Robert Parker" # one rep's accounts (name or id substring)
python3 run.py loop --tier strategic --limit 10
python3 run.py loop --domain-filter # Exa: account's own domain only
python3 run.py loop --no-domain-filter # Exa: broad-web news (default)
```
| Flag | Effect |
|---|---|
| `--account ID` | Run a single account by `accountId` (OEC number) |
| `--tier T` | Only `strategic` / `enterprise` / `growth` accounts |
| `--rep NAME` | Only accounts assigned to a rep (case-insensitive name or `rep_id` substring) |
| `--limit N` | Process at most N accounts. Order: **strategic tier first, then least-recently-scanned** (never-scanned first). The run cursor (`output/state/run_cursor.json`) persists, so successive `--limit` runs walk the whole territory — e.g. a nightly `loop --limit 30` covers 342 accounts in ~12 days. |
| `--capability SET` | Capability set to reason against: `ALL` (full 40-capability portfolio, default) or a named set from `data/capability_sets/` (e.g. `agentminder`). In a targeted set the keyword pre-filter is bypassed so EVERY signal is evaluated against the chosen product. List sets: `python3 run.py capability-sets`. **Add a new one-off product**: drop `data/capability_sets/<name>.json` (same schema as `capabilities.json`) and pass `--capability <name>` — no code changes. |
| `--domain-filter` | Scope Exa news to the account's own domain (newsroom/PR only) |
| `--no-domain-filter` | Broad-web news about the account (default) |
Per-account flow: harvest (Exa news + SEC EDGAR filings + stubs) → dedup →
**quality gate** (drops awards/CSR/ticker chatter pre-LLM; log:
`output/state/quality_gate_log.json`) → LLM relevance filter (with rolling
**account narrative** as context) → LLM synthesis (briefs with **cited talking
points** `[S#]` + updated narrative) → **citation verification** (uncited
points dropped; brief skipped if none survive) → scoring → delivery
(`output/briefs/*.md`) → memory + cursor update.
On LLM rate-limit (429): waits and retries (no fabricated output). On LLM
failure: account skipped, its signals retried next run.
### `digest` — per-rep intelligence digest
```bash
python3 run.py digest # text digest, all reps (also -> output/digest.md)
python3 run.py digest --rep rep_robert_parker # filter by rep id
python3 run.py digest --rep "Misty Brew" --html # forwardable HTML -> output/digests/
python3 run.py digest --html # HTML for all reps
```
| Flag | Effect |
|---|---|
| `--rep X` | Filter to one rep (text mode: rep id; HTML mode: name substring or id) |
| `--html` | Write a self-contained HTML file (`output/digests/digest_<rep>_<date>.html`) — clean enough to paste/forward as an email body |
### `renewals` — upcoming renewals by month (local entitlement data)
```bash
python3 run.py renewals # next 6 months
python3 run.py renewals --months 12 --limit 30
```
| Flag | Effect |
|---|---|
| `--months N` | Horizon (default 6) |
| `--limit N` | Max rows per month (default 15) |
Renewal proximity also feeds scoring automatically: a brief whose capability
(or account) has a renewal within `RENEWAL_WINDOW_DAYS` (default 365) gets a
boost up to `RENEWAL_BOOST_MAX` (default +0.10; half for account-level
renewals), scaled by closeness — computed locally, no LLM. Boosted briefs show
a **💰 RENEWAL WINDOW** block in the brief/digest.
### `whitespace` — territory coverage report
```bash
python3 run.py whitespace # prints + writes output/whitespace.md
```
Shows: scanned vs never-scanned vs stale (>14d) accounts, opportunities by
capability, pipeline status counts, per-account coverage, and the backlog of
never-scanned accounts. (Becomes a true owns-vs-whitespace matrix once
entitlement data populates `currentProducts`.)
### `feedback` — record a rep's verdict on a brief
```bash
python3 run.py feedback --brief <BRIEF_ID> --action act_on
python3 run.py feedback --brief <BRIEF_ID> --action reject --note "already own this"
```
| Flag | Effect |
|---|---|
| `--brief ID` | Brief id (see `briefs` command or the digest) |
| `--action A` | `act_on` \| `snooze` \| `reject` \| `won` \| `lost` |
| `--note "..."` | Optional free-text context, stored with the feedback |
### `recalibrate` — recompute feedback-driven scoring weights
```bash
python3 run.py recalibrate # weekly job; needs >=3 samples per combo
```
### `accounts` / `briefs` / `exa-usage` — inspection
```bash
python3 run.py accounts # list all target accounts (tier, rep, products)
python3 run.py briefs # all briefs, ranked by score, with status
python3 run.py exa-usage # Exa request budget (monthly cap / remaining)
```
### Account import (repeatable)
```bash
python3 scripts/import_accounts.py "/path/to/ISG Customer Sales Assignments.csv"
```
Rebuilds `data/accounts.json` from a territory CSV export (see script header
for the field mapping). Backs up nothing — copy `data/accounts.json` first.
### Entitlement import (repeatable, 100% local)
```bash
python3 scripts/import_entitlements.py data/SE_ACL_FY26_Q3.csv
```
Loads the confidential ACL export **entirely locally** (no LLM, no API):
aggregates active contract lines per parent, joins to accounts by normalized
parent name (the FY26 Q3 export's Parent Account ID column is Excel-corrupted),
maps product families → capability ids via `data/product_capability_map.json`,
and writes:
- `data/accounts.json``currentProducts` (readable, fed to LLM prompts) +
`ownedCapabilityIds` (used for local anti-signal checks)
- `data/entitlements.json` (**gitignored**) — full per-account detail incl.
next renewal dates
- `output/state/entitlement_unmapped.json` — product families with no
capability mapping yet; extend `product_capability_map.json` from this
During the loop, a brief for an already-owned capability is tagged
**⬆ EXPANSION** (local set lookup, no LLM call). The ACL CSV and
entitlements.json are gitignored and never leave the machine.
---
## Configuration (`.env`)
### LLM provider
| Var | Default | Meaning |
|---|---|---|
| `LLM_PROVIDER` | `claude-cli` | `claude-cli` (subscription via Claude Code headless) \| `anthropic` (direct API key) \| `openai` \| `gemini` |
| `CLAUDE_CLI_MODEL` | `sonnet` | Model alias for the `claude -p` headless provider |
| `CLAUDE_CLI_TIMEOUT` | `300` | Per-call timeout (s) for the headless CLI |
| `CLAUDE_CLI_LIMIT_BUFFER_SECONDS` | `60` | On a session-limit hit, pause until the stated reset time **plus this** (1 min past), then retry |
| `CLAUDE_CLI_LIMIT_MAX_WAITS` | `6` | Max reset windows to wait through in one call before giving up |
| `CLAUDE_CLI_LIMIT_FALLBACK_WAIT_SECONDS` | `3600` | Wait used only when no reset time can be parsed from the CLI message |
| `LLM_MODEL` | `claude-sonnet-5` | Anthropic model id (direct-API `anthropic` provider) |
| `ANTHROPIC_EFFORT` | `low` | Reasoning effort: `low` \| `medium` \| `high` \| `max` |
| `ANTHROPIC_USE_KEYCHAIN` | `1` | Read the Claude Code OAuth token from the macOS keychain (kept fresh by Claude Code; refresh with any `claude -p ...` call if expired) |
| `ANTHROPIC_OAUTH_TOKEN` | — | Explicit OAuth token (overrides keychain) |
| `ANTHROPIC_API_KEY` | — | Classic API key (used only if no OAuth token available) |
| `OPENAI_BASE_URL` | — | Any OpenAI-compatible `/chat/completions` endpoint (NVIDIA, LM Studio, ... — commented examples in `.env`) |
| `OPENAI_API_KEY` / `OPENAI_MODEL` | — | Credentials/model for that endpoint |
| `OPENAI_JSON_MODE` | `1` | Send `response_format=json_object` (0 if endpoint rejects it) |
| `OPENAI_TEMPERATURE` | `0.5` | Sampling (OpenAI-compatible only) |
| `OPENAI_FREQUENCY_PENALTY` | `0.4` | Anti-repetition (OpenAI-compatible only) |
| `GEMINI_API_KEY` / `GEMINI_MODEL` | — | Gemini credentials/model |
| `LLM_RATELIMIT_WAIT_SECONDS` | `900` | Wait per 429 before retry (honors `Retry-After`) |
| `LLM_RATELIMIT_MAX_RETRIES` | `24` | Max 429 waits before skipping the account |
| `LLM_CONTENT_RETRIES` | `3` | Re-samples on degenerate/unparseable model output |
**Switching providers** is a `.env` edit only: set `LLM_PROVIDER`, uncomment the
relevant block (the NVIDIA endpoint is preserved, commented, for switch-back).
### Signal sources
| Var | Default | Meaning |
|---|---|---|
| `EXA_API_KEY` | — | Exa.ai key (news search) |
| `EXA_ENABLED` | `1` | `0` forces fixtures (no API calls) |
| `EXA_MONTHLY_REQUEST_CAP` | `1000` | Hard monthly ceiling (persistent counter) |
| `EXA_PER_RUN_REQUEST_CAP` | `25` | Hard per-run ceiling |
| `EXA_RESULTS_PER_QUERY` | `5` | Articles per account per run |
| `EXA_LOOKBACK_DAYS` | `45` | News/filings recency window |
| `EXA_CATEGORY` | `news` | Exa category filter (`""` disables) — keeps out careers/marketing pages |
| `EXA_DOMAIN_FILTER` | `0` | `1` = first-party newsroom only |
| `EXA_EXCLUDE_DOMAINS` | — | Comma-separated hosts to always exclude |
| `SEC_ENABLED` | `1` | SEC EDGAR filings harvester (free, public accounts auto-detected via CIK match) |
| `SEC_MAX_FILINGS_PER_ACCOUNT` | `5` | Max recent filings per account per run |
### Scoring
| Var | Default | Meaning |
|---|---|---|
| `PRIORITY_THRESHOLD` | `0.72` | Composite score for priority (immediate) delivery |
| `STANDARD_THRESHOLD` | `0.45` | Composite score for standard (digest) delivery |
| `CONFIDENCE_FLOOR` | `0.35` | Briefs the LLM itself scores below this are logged, never delivered |
| `RENEWAL_WINDOW_DAYS` | `365` | Renewal proximity window for the score boost |
| `RENEWAL_BOOST_MAX` | `0.10` | Max boost at renewal date (same-capability; half for account-level) |
Composite formula: `signalStrength*0.30 + llmConfidence*0.20 + accountTier*0.20 +
buyerSeniority*0.15 + timingUrgency*0.10 + repFeedback*0.05`.
---
## Output layout
```
output/
├── briefs/ # delivered briefs (Slack-style markdown)
├── digests/ # HTML digests (digest_<rep>_<date>.html)
├── digest.md # latest text digest (convenience copy)
├── digest_<date>_<time>.md # timestamped digests (every run kept)
├── whitespace.md # latest coverage report
└── state/
├── run_cursor.json # per-account last-scanned timestamps (--limit batching)
├── memory_<accountId>.json # per-account memory incl. accountNarrative
├── briefs_index.json # all briefs + status
├── feedback.json # rep feedback records
├── scoring_weights.json # recalibrated weights
├── quality_gate_log.json # last 500 signals dropped pre-LLM (tune the gate here)
├── exa_usage.json # Exa monthly request counter
├── sec_company_tickers.json # SEC company->CIK table (cached 7 days)
└── sec_cik_map.json # accountId->CIK resolution cache (incl. non-public misses)
```
## Typical cadences
```bash
# Nightly (cron): walk the territory 30 accounts at a time
python3 run.py loop --limit 30
# Monday morning: coverage review + per-rep digests
python3 run.py whitespace
python3 run.py digest --rep "Misty Brew" --html
# Weekly: fold rep feedback into scoring
python3 run.py recalibrate
```