Rules

The scoring formula, the policy gate, the state machine, and the capability boundaries this system enforces — copied by hand from the real source (file names given throughout) and re-checked against it, not re-derived here. This page renders no database query at all; it's the same class of artifact as docs/policy-matrix.md, just in one screen instead of eight files.

1. Lead scoring — a 0–100 formula, five sub-scores

Computed once per target by score_lead (app/tools/score_lead.py), immediately after research. Every sub-score is a pure function of what the research stage actually found — nothing here is guessed.

Sub-scoreMaxHow it's computed
Company fit 30 Industry identified (+10) + estimated size identified (+10) + the research LLM's own confidence in the profile, scaled to 0–10.
Persona fit 15 (spec allows 20; the "seniority sufficient" 5pts needs title parsing this phase doesn't have) 15 if the target carries contact data (name/title from the CSV import), 0 otherwise — 0 is the common case, since Phase 1 has no contact-enrichment step.
Buying signals 25 Sum of each detected signal's strength (0.0–1.0) × its type's weight, capped at 25 — signal quality matters more than quantity. Weights: hiring for a related role (8), a product/ops change (7), a recent launch or expansion (5), workflow-complexity evidence (5).
Data completeness 15 (10 reachable without contact data) Readable website content (+5) + contact identified (+5) + verified email (+5, not implemented in Phase 1 — always 0, not guessed).
Evidence quality 10 At least one signal AND profile confidence ≥ 0.5 (+5, a proxy for "≥2 independent sources") + no detected contradiction (+5, unconditional in Phase 1 — there's no multi-source enrichment yet to contradict).

The five caps sum to 100, so the formula can never exceed its own scale without an extra clamp — the caps are the clamp. In practice a contact-less target (the common case) tops out around 70; even a target with full contact data tops out around 95, because email verification isn't built in this phase. That ceiling is stated honestly here rather than rounded up.

Fit label — what the score routes to

ScoreLabelWhat happens next
≥ 80strong_fitStays in the active pipeline, eligible to draft.
≥ 60good_fitStays in the active pipeline, eligible to draft.
≥ 40watchlistRouted out — insufficient signal, re-checked later.
< 40not_targetRouted out — below the bar, dropped from the active pipeline.

The LLM judge — a verdict, never a number

A second model (icp_judge) reviews the same evidence and may override the label above — but only with a written justification, or its response is rejected outright. Its output schema has no score field at all, so it structurally cannot touch the number: the policy floor below always reads the deterministic score the formula produced, never the judge's opinion. A judge that calls a 51-point target strong_fit changes the target's routing label, but the policy decision on that target still records a deny with rule P4 — the floor holds regardless of what the judge argues.

2. Policy gate — nine rules, one choke point

policy_check (app/policy.py) is the single place every action that could produce an external effect or a state change gets checked. It never fails silently — an action matching no rule below resolves to deny, not allow.

RuleWhat it enforces
P1No send without an operator approved decision — ever.
P2No send to any address present in suppressions.
P3 / P3aRequired fields must be present before drafting or sending (the Phase 1 variant excludes fields this phase has no enrichment to populate, e.g. verified email — never silently treated as passing).
P4Confidence floor: fit score < 60 → deny. Draft or reply-classification confidence < 0.7 → routed to a human, never auto-acted.
P5Reply classes legal, complaint, privacy, risky always route to a human, at any confidence.
P6The kill switch dominates every other rule, unconditionally, while engaged. Read uncached, fail-closed — a missing or malformed switch file reads as engaged. De-escalating actions (reject, suppress, escalate) still work while engaged on purpose: the emergency stop must not also lock the operator out of the brakes.
P7No autonomous re-send: a rolling-window cap on drafts, plus a hard, separately-enforced cap of 2 follow-up drafts per reply thread, ever — counted directly from the state-transition history, never trusted from a prompt.
P8Scraped or inbound text carrying injection markers ("ignore previous", oversized base64 blobs, etc.) is flagged and routed to a human rather than drafted from. Inbound email is redacted before any model ever reads it.
P9A field enrichment conflict (two sources disagreeing on an email or title) routes to a human rather than silently picking one.

Underneath the numbered rules: per-agent capability enforcement

Every core-table write passes through write_gate.commit(), which checks three things before any SQL runs — a rejected write names both the agent and the action, so a denial is observable, never silent:

CheckRefuses when
RegisteredNo agent_registry row exists for the calling agent id.
EnabledThe agent's registry row has enabled = 0 — a per-agent kill switch.
AuthorizedThe requested action isn't in that agent's own allowed_actions list.

This is what makes an agent structurally unable to take an action, rather than merely instructed against it: the research agent handles untrusted scraped text and would be refused any write outside its capability set even if its own prompt were subverted.

The natural-language agent's four boundaries

The TaskmasterAgent (python -m app.taskmaster_cli) can plan and dispatch the whole pipeline from one sentence — but each of the following is something it cannot do, enforced in code and pinned by a test that fails if the capability is ever added:

BoundaryEnforced by
Cannot approve anythingIts registry row's allowed actions is the empty list — even a hypothetical write attributed to it is refused by the gate before any SQL runs.
Cannot send live emailNo mail transport exists anywhere in this repository — an AST test walks every module and fails if one ever appears.
Cannot disable the kill switchNo tool it owns references the switch's writer; it may only read the switch.
Cannot exceed the batch capA natural-language batch over 15 targets is refused deterministically, in code, before any database or file I/O — the model can report the cap, never negotiate it.

3. State machine — every valid move, nothing implicit

No component transitions a target directly — every change goes through state_machine.transition(), validated against a fixed table (app/state_machine.py). A transition not in this table is refused; there is no LLM-decided or implicit state change anywhere in this system.

The 16 states

newenriched / researchedscoreddraftedawaiting_reviewapprovedsent / dry_run_sentrepliedrouted, with watchlist, not_target, bounced, suppressed, and failed as the exits along the way.

The path a target actually takes

StageFrom → ToTrigger
Research & scorenew → researchedResearch succeeds
researched → scoredScoring succeeds
scored → draftedPolicy allows a draft (P3a + P4 pass)
Routed awayscored → watchlistInsufficient signal
scored → not_targetNon-ICP or missing critical fields
Reviewdrafted → awaiting_reviewDraft complete
awaiting_review → approvedOperator approves
awaiting_review → not_targetOperator rejects
awaiting_review → researchedOperator escalates for more research
Sendapproved → sentSend gate passes, LIVE mode
approved → dry_run_sentSend gate passes, DRY_RUN mode
Replysent / dry_run_sent → repliedAn inbound (or simulated) reply is linked
replied → routedClassifier verdict + router decision
routed → draftedA positive reply queues a real follow-up draft
Terminalrouted → suppressedReply class is unsubscribe
bounced → suppressedHard bounce, automatic
any state → suppressed / failedAn address lands in the suppression list, or an unrecoverable error — reachable from every state, not one hop from a fixed origin