franco 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. franco-0.5.0/.env.example +120 -0
  2. franco-0.5.0/CHANGELOG.md +304 -0
  3. franco-0.5.0/LICENSE +22 -0
  4. franco-0.5.0/MANIFEST.in +7 -0
  5. franco-0.5.0/PKG-INFO +706 -0
  6. franco-0.5.0/README.md +666 -0
  7. franco-0.5.0/franco/__init__.py +19 -0
  8. franco-0.5.0/franco/__main__.py +6 -0
  9. franco-0.5.0/franco/agents/__init__.py +0 -0
  10. franco-0.5.0/franco/agents/analyst.py +187 -0
  11. franco-0.5.0/franco/agents/handler.py +795 -0
  12. franco-0.5.0/franco/agents/scout.py +1873 -0
  13. franco-0.5.0/franco/agents/sender.py +640 -0
  14. franco-0.5.0/franco/agents/writer.py +843 -0
  15. franco-0.5.0/franco/brain.py +699 -0
  16. franco-0.5.0/franco/cli.py +1276 -0
  17. franco-0.5.0/franco/codex.py +1531 -0
  18. franco-0.5.0/franco/collectors/__init__.py +9 -0
  19. franco-0.5.0/franco/collectors/discover.py +913 -0
  20. franco-0.5.0/franco/collectors/profile.py +517 -0
  21. franco-0.5.0/franco/config.py +591 -0
  22. franco-0.5.0/franco/dashboard.py +1634 -0
  23. franco-0.5.0/franco/export.py +125 -0
  24. franco-0.5.0/franco/funnels.py +176 -0
  25. franco-0.5.0/franco/gate.py +121 -0
  26. franco-0.5.0/franco/importer.py +495 -0
  27. franco-0.5.0/franco/integrations/__init__.py +8 -0
  28. franco-0.5.0/franco/integrations/calendar.py +26 -0
  29. franco-0.5.0/franco/integrations/email_finder.py +614 -0
  30. franco-0.5.0/franco/integrations/gmail.py +340 -0
  31. franco-0.5.0/franco/integrations/instantly.py +299 -0
  32. franco-0.5.0/franco/integrations/linkedin.py +433 -0
  33. franco-0.5.0/franco/integrations/mail_provider.py +101 -0
  34. franco-0.5.0/franco/integrations/quota.py +196 -0
  35. franco-0.5.0/franco/integrations/smtp_mail.py +191 -0
  36. franco-0.5.0/franco/integrations/tech_detect.py +92 -0
  37. franco-0.5.0/franco/main.py +426 -0
  38. franco-0.5.0/franco/models/__init__.py +16 -0
  39. franco-0.5.0/franco/models/campaign.py +44 -0
  40. franco-0.5.0/franco/models/company.py +47 -0
  41. franco-0.5.0/franco/models/conversation.py +56 -0
  42. franco-0.5.0/franco/models/prospect.py +58 -0
  43. franco-0.5.0/franco/outreach.py +199 -0
  44. franco-0.5.0/franco/paths.py +84 -0
  45. franco-0.5.0/franco/pipeline.py +129 -0
  46. franco-0.5.0/franco/setup.py +723 -0
  47. franco-0.5.0/franco/signals.py +331 -0
  48. franco-0.5.0/franco/state.py +2179 -0
  49. franco-0.5.0/franco/trainer.py +824 -0
  50. franco-0.5.0/franco/usage.py +27 -0
  51. franco-0.5.0/franco/web/app.css +595 -0
  52. franco-0.5.0/franco/web/app.js +1602 -0
  53. franco-0.5.0/franco/web/fonts/geist-mono.woff2 +0 -0
  54. franco-0.5.0/franco/web/fonts/geist.woff2 +0 -0
  55. franco-0.5.0/franco/web/fonts/newsreader.woff2 +0 -0
  56. franco-0.5.0/franco/web/index.html +422 -0
  57. franco-0.5.0/franco.egg-info/PKG-INFO +706 -0
  58. franco-0.5.0/franco.egg-info/SOURCES.txt +115 -0
  59. franco-0.5.0/franco.egg-info/dependency_links.txt +1 -0
  60. franco-0.5.0/franco.egg-info/entry_points.txt +2 -0
  61. franco-0.5.0/franco.egg-info/requires.txt +19 -0
  62. franco-0.5.0/franco.egg-info/top_level.txt +1 -0
  63. franco-0.5.0/franco.yaml +101 -0
  64. franco-0.5.0/prompts/handler.md +36 -0
  65. franco-0.5.0/prompts/scout.md +36 -0
  66. franco-0.5.0/prompts/system.md +21 -0
  67. franco-0.5.0/prompts/writer.md +59 -0
  68. franco-0.5.0/pyproject.toml +64 -0
  69. franco-0.5.0/requirements.in +22 -0
  70. franco-0.5.0/requirements.txt +428 -0
  71. franco-0.5.0/setup.cfg +4 -0
  72. franco-0.5.0/setup.py +45 -0
  73. franco-0.5.0/skills/README.md +48 -0
  74. franco-0.5.0/skills/account_navigation.md +108 -0
  75. franco-0.5.0/skills/email_frameworks.md +301 -0
  76. franco-0.5.0/skills/lead_qualification.md +82 -0
  77. franco-0.5.0/skills/linkedin_outreach.md +141 -0
  78. franco-0.5.0/skills/objection_handling.md +110 -0
  79. franco-0.5.0/skills/offer_strategy.md +144 -0
  80. franco-0.5.0/skills/prospecting_tactics.md +148 -0
  81. franco-0.5.0/skills/sales_methodology.md +150 -0
  82. franco-0.5.0/skills/signal_playbook.md +88 -0
  83. franco-0.5.0/tests/test_approved_copy.py +351 -0
  84. franco-0.5.0/tests/test_autopilot.py +182 -0
  85. franco-0.5.0/tests/test_brain_no_tools.py +51 -0
  86. franco-0.5.0/tests/test_brain_provider.py +750 -0
  87. franco-0.5.0/tests/test_code_scanning_fixes.py +121 -0
  88. franco-0.5.0/tests/test_codex.py +863 -0
  89. franco-0.5.0/tests/test_codex_cli_surface.py +428 -0
  90. franco-0.5.0/tests/test_codex_e2e.py +820 -0
  91. franco-0.5.0/tests/test_codex_platform.py +195 -0
  92. franco-0.5.0/tests/test_codex_soak.py +230 -0
  93. franco-0.5.0/tests/test_collectors_profile.py +157 -0
  94. franco-0.5.0/tests/test_config.py +61 -0
  95. franco-0.5.0/tests/test_config_extra.py +174 -0
  96. franco-0.5.0/tests/test_dashboard_outbox.py +157 -0
  97. franco-0.5.0/tests/test_dashboard_signals.py +255 -0
  98. franco-0.5.0/tests/test_dependencies.py +68 -0
  99. franco-0.5.0/tests/test_discover.py +661 -0
  100. franco-0.5.0/tests/test_email_finder.py +256 -0
  101. franco-0.5.0/tests/test_email_wiring.py +140 -0
  102. franco-0.5.0/tests/test_importer.py +254 -0
  103. franco-0.5.0/tests/test_models.py +62 -0
  104. franco-0.5.0/tests/test_models_extra.py +145 -0
  105. franco-0.5.0/tests/test_outbox_native.py +351 -0
  106. franco-0.5.0/tests/test_outreach.py +224 -0
  107. franco-0.5.0/tests/test_packaging.py +222 -0
  108. franco-0.5.0/tests/test_paths.py +126 -0
  109. franco-0.5.0/tests/test_pipeline.py +290 -0
  110. franco-0.5.0/tests/test_raad.py +410 -0
  111. franco-0.5.0/tests/test_release_notes.py +47 -0
  112. franco-0.5.0/tests/test_replies.py +160 -0
  113. franco-0.5.0/tests/test_signals_export.py +157 -0
  114. franco-0.5.0/tests/test_state.py +115 -0
  115. franco-0.5.0/tests/test_state_extra.py +309 -0
  116. franco-0.5.0/tests/test_state_phase1.py +71 -0
  117. franco-0.5.0/tests/test_usage.py +164 -0
@@ -0,0 +1,120 @@
1
+ # ============================================================
2
+ # Franco environment variables
3
+ # Copy this file to .env and fill in your values: cp .env.example .env
4
+ # Never commit .env — it is gitignored.
5
+ # Pick ONE email provider below (set channels.email.provider in franco.yaml
6
+ # to match): gmail (recommended), smtp, or instantly. Everything else is
7
+ # optional but an email verifier (Reoon/ZeroBounce) is strongly recommended.
8
+ # ============================================================
9
+
10
+ # ------------------------------------------------------------
11
+ # EMAIL PROVIDER — how Franco sends and reads replies. Choose one.
12
+ #
13
+ # Option A: Gmail / Google Workspace [RECOMMENDED for <50 cold/day]
14
+ # Set provider: gmail in franco.yaml. Use a mailbox on a DEDICATED
15
+ # secondary domain (never your main one), with SPF/DKIM/DMARC set up.
16
+ # 1. Google Cloud Console → new project → enable the Gmail API
17
+ # 2. Credentials → OAuth client ID → type "Desktop app"
18
+ # 3. Paste the client id/secret below, then run: franco gmail auth
19
+ GMAIL_CLIENT_ID=
20
+ GMAIL_CLIENT_SECRET=
21
+
22
+ # Option B: Any SMTP + IMAP mailbox [AgentMail, Fastmail, Workspace app pw]
23
+ # Set provider: smtp in franco.yaml. IMAP_* defaults to the SMTP_* values.
24
+ SMTP_HOST=
25
+ SMTP_PORT=587
26
+ SMTP_USERNAME=
27
+ SMTP_PASSWORD=
28
+ IMAP_HOST=
29
+ IMAP_PORT=993
30
+ IMAP_USERNAME=
31
+ IMAP_PASSWORD=
32
+
33
+ # Option C: Instantly — cold email platform [legacy]
34
+ # Where to get it: https://app.instantly.ai → Settings → Integrations → API Keys
35
+ # API access requires Instantly's Growth plan or higher.
36
+ INSTANTLY_API_KEY=
37
+
38
+ # ------------------------------------------------------------
39
+ # LinkedIn — browser-automation prospecting [OPTIONAL]
40
+ # These are the credentials of the LinkedIn account Franco will log into
41
+ # with Playwright. WARNING: automation violates LinkedIn's Terms of Service
42
+ # and can get the account restricted or banned. Use a account you can
43
+ # afford to lose, keep the default rate limits, or leave blank to skip —
44
+ # Franco will prospect via web search and company websites instead.
45
+ LINKEDIN_EMAIL=
46
+ LINKEDIN_PASSWORD=
47
+
48
+ # ------------------------------------------------------------
49
+ # Cloudflare Browser Rendering — deep website crawling during training [OPTIONAL]
50
+ # Enables JS-rendered crawling of your own site when running `franco train`.
51
+ # Where to get it:
52
+ # Account ID: https://dash.cloudflare.com → right sidebar of any zone, or the URL
53
+ # API Token: https://dash.cloudflare.com/profile/api-tokens → Create Token
54
+ # → grant "Browser Rendering: Edit" permission
55
+ # Cost: ~$5/mo on the paid Workers plan (~12,000 pages). Free tier has limits.
56
+ # Without these, Franco uses its built-in HTTP crawler (no JS rendering).
57
+ CLOUDFLARE_ACCOUNT_ID=
58
+ CLOUDFLARE_API_TOKEN=
59
+
60
+ # ------------------------------------------------------------
61
+ # Email verification — how Franco confirms a prospect's address really
62
+ # exists before sending. Franco learns each company's email PATTERN, then
63
+ # verifies ONE candidate (raw SMTP probing no longer works against Google
64
+ # Workspace / Microsoft 365). Any ONE of these keys is enough; add both for
65
+ # best coverage. Without any, addresses are marked "guess" and never sent.
66
+
67
+ # Reoon — best free tier for cold-email verification (600 checks/month,
68
+ # renews monthly + 100 non-expiring). Good catch-all flagging.
69
+ # Where to get it: https://reoon.com/email-verifier → sign up → API
70
+ REOON_API_KEY=
71
+
72
+ # ZeroBounce — 100 free/month. Its 2026 engine is the reliable one for
73
+ # Microsoft 365 / Google Workspace catch-all domains; Franco routes those
74
+ # domains here specifically. https://zerobounce.net → API
75
+ ZEROBOUNCE_API_KEY=
76
+
77
+ # Hunter.io — email discovery + pattern lookup [OPTIONAL]
78
+ # Free tier: 50 verifications + 25 domain-searches/month. Its domain-search
79
+ # is the cheapest way to learn a company's email pattern (used first, so
80
+ # Franco verifies one address instead of guessing six).
81
+ # Where to get it: https://hunter.io → sign up → API → API Keys
82
+ HUNTER_API_KEY=
83
+ # Domain-searches per month across all of Franco. 25 matches Hunter's free
84
+ # tier; set this to your plan's allowance if you pay for Hunter.
85
+ HUNTER_MONTHLY_SEARCHES=25
86
+
87
+ # ------------------------------------------------------------
88
+ # DISCOVERY — where Franco finds businesses. [ALL OPTIONAL]
89
+ # The default source (OpenStreetMap) needs no account at all:
90
+ # franco discover
91
+ # See every option, with real prices, via:
92
+ # franco discover --providers (or the dashboard's Discover tab)
93
+ # ------------------------------------------------------------
94
+
95
+ # DataForSEO — cheapest local-business and SERP data anywhere.
96
+ # Where to get it: https://app.dataforseo.com/api-access
97
+ # NOTE: the API password is auto-generated there. It is NOT your account
98
+ # password. $1 free credit on signup; $50 minimum deposit to go live.
99
+ # Cost: $0.372 per 1,000 businesses; $0.0018 per depth-30 search.
100
+ DATAFORSEO_LOGIN=
101
+ DATAFORSEO_PASSWORD=
102
+ # Set to any value to route every call to DataForSEO's free sandbox. Same
103
+ # response shape, dummy data, never charged — the right way to try it.
104
+ DATAFORSEO_SANDBOX=
105
+
106
+ # Serper.dev — Google Search API, used for discovery and for Scout's searches.
107
+ # Where to get it: https://serper.dev → sign up → Dashboard → API Key
108
+ # Cost: 2,500 queries free with no card, then a $50 prepaid pack (credits
109
+ # expire after 6 months). Without it Franco falls back to DuckDuckGo/Bing
110
+ # scraping, which is heavily rate-limited.
111
+ SERPER_API_KEY=
112
+
113
+ # ------------------------------------------------------------
114
+ # Calendly — meeting auto-booking [NOT YET USED — roadmap]
115
+ # CALENDLY_API_KEY=
116
+
117
+ # --- Dashboard (optional) ---
118
+ # The dashboard only answers requests addressed to localhost / 127.0.0.1.
119
+ # Add other hostnames you reach it by (e.g. from Docker), comma-separated.
120
+ FRANCO_DASHBOARD_HOSTS=
@@ -0,0 +1,304 @@
1
+ # Changelog
2
+
3
+ Notable changes to Franco. Dates are release dates; the format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loosely and versions
5
+ follow [semver](https://semver.org/), with the caveat that Franco is pre-1.0 and
6
+ minor versions can still change behaviour.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.5.0] — 2026-10-16
11
+
12
+ ### Added
13
+
14
+ - **`pip install franco`.** Franco is on [PyPI](https://pypi.org/project/franco/) and works without a checkout:
15
+ - `franco init [DIR]` creates a **home folder**: `franco.yaml`, a private `.env`, editable `prompts/` and `skills/`, `data/` and a `.gitignore` that keeps credentials and contact data out of git. It never overwrites existing files.
16
+ - Franco finds its home as `$FRANCO_HOME`, else the source checkout (unchanged for clones and Docker), else the current folder when it has a `franco.yaml`, else `~/.franco`.
17
+ - The wheel bundles the default prompts, skills and templates. A file in the home folder wins; delete it to fall back to the shipped version.
18
+ - A pip-installed Franco never writes into its installed package.
19
+ - `franco install` only fetches Chromium when Franco was installed from PyPI.
20
+ - **Releases publish to PyPI** with trusted publishing (no stored token) and PEP 740 attestations, before the GitHub Release is created.
21
+
22
+ ### Fixed
23
+
24
+ - `franco train` created `skills/` files without creating the folder first, which failed in a fresh home folder.
25
+ - The dashboard's setup check counted only a `.venv` inside the project as a virtual environment.
26
+
27
+
28
+ ## [0.4.0] — 2026-10-16
29
+
30
+ Franco is now open source under [GortexAI/franco](https://github.com/GortexAI/franco), published as a container image, and released automatically.
31
+
32
+ ### Upgrading from 0.3.0
33
+
34
+ - **Set your postal address.** The template `franco.yaml` now ships with `compliance.require_footer: true` and a blank `postal_address`. No cold email sends until you enter your own address. If your config predates this, the footer stays off until you turn it on.
35
+ - **The dashboard only answers on localhost.** If you reach it under another hostname (from Docker, for example), list it in `FRANCO_DASHBOARD_HOSTS`.
36
+ - New config sections (`outreach`, `compliance`) all have defaults. Existing configs load unchanged.
37
+
38
+ ### Added
39
+
40
+ - **Approved-copy outreach** (`outreach.mode: templates`):
41
+ - Signed-off sequences in `outreach.campaigns` are sent word for word to researched accounts, with no model involved.
42
+ - Merge fields are limited to `{{first_name}}`, `{{last_name}}`, `{{company}}`, `{{title}}`, `{{sender_name}}` and `{{verified_observation}}`. The observation is a sourced, dated fact recorded per account. An account with an unfilled field is skipped, never sent with a gap.
43
+ - Steps are scheduled in business days in `outreach.timezone`, and each follow-up is re-timed from the day the previous step actually went out.
44
+ - **Raad mode** (`outreach.mode: raad`), named after Raad Mobrem, CEO of Intro, whose outbound approach it follows:
45
+ - Up to three `outreach.benefits`; every funnel names exactly one.
46
+ - `outreach.funnels` are narrow segments matched on observed signals (`TECH_STACK=AdSense|Prebid`) or account fields (`segment=marketplace`), tried narrowest first.
47
+ - Email 1 is written from the funnel's story, the account's sourced facts, the one benefit and a role-specific angle, then the funnel's approved follow-ups are queued.
48
+ - `franco funnels` (and the dashboard's Campaigns tab) shows how companies split across funnels; `--company DOMAIN` explains one placement.
49
+ - The profiler now detects publisher ad tags (Google AdSense, Ad Manager, Prebid, Amazon Publisher Services).
50
+ - **Bring your own lists as CSV files.** `franco import FILE --kind accounts|contacts|suppressions`, plus an Import CSV panel on the dashboard's Contacts tab:
51
+ - Each kind has a template (`--template`), header aliases, `--dry-run` and optional `--verify`.
52
+ - Every row is validated before anything is written.
53
+ - `franco suppress import` parses real CSV (with a `reason` column, or one address per line), and `export` quotes properly.
54
+ - **Preview and sign-off.** New approved-copy campaigns and raad funnels start in preview (`outreach.preview_accounts`, default 10 per owner):
55
+ - Preview emails always wait for review, even on autopilot, and nothing sends until someone signs off.
56
+ - Commands: `franco preview [KEY] [--csv FILE]`, `franco preview signoff KEY --by NAME`, `franco preview reopen KEY`. The Outbox shows a sign-off card for each campaign in preview.
57
+ - **Compliance in code:**
58
+ - `compliance.require_footer` puts the sender, company and postal address plus "Reply "no thanks" to stop." on every cold email in every mode. The gate refuses cold email without the footer, and nothing sends while the postal address is blank.
59
+ - Cold email gets List-Unsubscribe headers (mailto, plus optional one-click HTTPS).
60
+ - `compliance.blocked_claims` is enforced on every email, replies included.
61
+ - A shared suppression list covers addresses and whole domains (`franco suppress`).
62
+ - **Account-level outreach:**
63
+ - One contact per account in a sequence at a time, and one owner per account.
64
+ - A reply or opt-out from anyone stops queued email to everyone at that company.
65
+ - Follow-ups wait for the previous step, and are cancelled if it was rejected or failed.
66
+ - **Replies:**
67
+ - Quoted history and Franco's own footer are removed before any rule runs.
68
+ - "no thanks" (body or subject) is an opt-out, and is suppressed before the optional acknowledgement is sent.
69
+ - Out-of-office replies no longer end a sequence.
70
+ - A new `later` intent pauses the contact and records when to get back in touch.
71
+ - `outreach.reply_templates` provides approved wording per intent; `not_interested` sends its template as written.
72
+ - **Autopilot toggle.** `franco autopilot on|off|default` (and an Outbox button) overrides `channels.email.require_approval` at runtime; turning it on approves what is waiting. Every gate still applies.
73
+ - **Outbox context and editing.** Each draft shows why it exists: campaign, owner, buyer, observation with its source and the date checked. Drafts can be edited before approval with `franco outbox --edit ID` or `E` on the dashboard.
74
+ - **Container image** `ghcr.io/gortexai/franco`, for linux/amd64 and linux/arm64, with an SBOM and a signed build-provenance attestation:
75
+ - Pull requests build and smoke-test it.
76
+ - Every push to `main` publishes `:edge`, and each release publishes `:X.Y.Z`, `:X.Y` and `:latest`.
77
+ - `docker-compose.yml` uses the published image.
78
+ - **Release workflow.** Tagging `vX.Y.Z` checks the version against `pyproject.toml` and the CHANGELOG, builds and tests the sdist and wheel, and creates the GitHub Release. See "Releasing" in `CONTRIBUTING.md`.
79
+ - CI tests Python 3.11, 3.12, 3.13 and 3.14 on Linux, macOS and Windows.
80
+ - `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, issue forms and a pull request template.
81
+
82
+ ### Security
83
+
84
+ - **The dashboard refuses cross-site requests and DNS-rebound hosts.** Any web page could previously POST to `localhost:5555`, for example to turn on autopilot.
85
+ - **Raad-mode drafts are checked in code.** Facts reach the model as fenced, untrusted data. Code rejects any draft that contains a blocked claim, HTML, an email address, an IP address, or any link or domain other than the funnel's landing page (Unicode look-alike dots included).
86
+ - **Dependencies are locked and fully scanned.** `requirements.txt` is generated by `pip-compile` from `requirements.in`, with every package pinned and hashed.
87
+ - Dependabot alerts now cover all 28 packages, up from 12.
88
+ - Dependabot security updates work again; they had been failing without a lockfile.
89
+ - Renovate recompiles the lock and keeps `pyproject.toml`, `requirements.in` and the lock in step. `tests/test_dependencies.py` checks that they agree.
90
+ - **Every GitHub Action is pinned to a full commit SHA**, with Renovate keeping the pins current, so a moved or hijacked tag can't change what runs with publishing and signing permissions.
91
+ - **Fixed all 32 CodeQL alerts inherited from the original code:**
92
+ - **Path injection (4, high):** the dashboard serves only files listed under `franco/web/`, never a requested path.
93
+ - **Clear-text logging of secrets (6, high):** the setup wizard reads API keys and passwords through a separate hidden-input function. Secrets were only ever written to `.env`.
94
+ - **Bad HTML tag filter (1, high):** script and style stripping handles end tags like `</script >`.
95
+ - **Stack-trace exposure (20, medium):** dashboard endpoints and run reports log the details and show fixed wording. Franco's own explanations, such as "OpenStreetMap has no category for 'widgets'", are still shown as written.
96
+ - **Log injection (1, medium):** newlines are removed from logged search terms, provider names and errors.
97
+ - CSV exports (prospects, preview, suppressions) neutralise cells starting with `=`, `+`, `-` or `@`, so imported or scraped text can't run as a spreadsheet formula.
98
+ - Scraped website and LinkedIn URLs are linked only when they are http(s).
99
+ - Secret scanning with push protection, private vulnerability reporting, CodeQL and Code Quality are enabled on the repository.
100
+
101
+ ### Fixed
102
+
103
+ - **The Docker image never had the Claude CLI.** Its installer is a bash script but ran under `sh`, and `|| true` hid the failure, so containers couldn't make any model call. It now runs with bash, and the build fails if `claude` is missing.
104
+ - A `.dockerignore` keeps `.env`, `franco.local.yaml`, `data/` and the virtualenv out of local Docker builds.
105
+ - When several steps of a sequence were overdue at once, they no longer all send in the same cycle.
106
+ - Accounts staged on a weekend start on Monday, not Tuesday.
107
+ - No empty campaign rows when every account in a batch is skipped.
108
+ - A usage test no longer fails between 5pm and midnight Pacific (it seeded data with the UTC date while Franco uses the local one).
109
+
110
+ ## [0.3.0] — 2026-10-13
111
+
112
+ ### Added
113
+
114
+ - **Opt-in Codex brain (ChatGPT plan).** Set `brain.provider: codex` (or pick
115
+ it in `franco setup`, or `franco train --provider codex`) to think with the
116
+ Codex CLI on a ChatGPT subscription instead of Claude. Claude remains the
117
+ default and there is no automatic fallback between providers. Each call
118
+ runs a throwaway, isolated `codex app-server` with no tools; Franco verifies
119
+ the lockdown from Codex's own reports before sending any prompt, aborts on
120
+ any attempted tool use, refuses API-key logins and custom endpoints
121
+ (checked before your login is exposed to Codex), and never changes your own
122
+ Codex settings or login: each call gets a private copy of the login without
123
+ its refresh token, so an expired login must be renewed with Codex itself or
124
+ `codex login`. Requires Codex CLI 0.159.3; other versions are refused. See
125
+ the README section "Using a ChatGPT plan instead (Codex)".
126
+ - `brain.model` picks the model for either provider, for Franco only. For
127
+ Codex use `gpt-5.5` (suggested by `franco setup`): the verified text-only
128
+ model. Blank auto-selection only picks listed models without code-execution
129
+ tools, and on current ChatGPT accounts there are none; code-mode models are
130
+ refused.
131
+ - If Codex can't be used (expired or renewal-due login, unsupported CLI,
132
+ unsafe Codex config, rejected model, plan usage limit), Franco pauses the
133
+ heartbeat instead of running agents that would fail: replies stay unread
134
+ and unescalated, queued emails wait, and the reason shows in the log,
135
+ `franco status` and the dashboard. It resumes on its own once the cause is
136
+ fixed. `franco status --check` runs the same check (no prompt sent) on demand
137
+ and exits non-zero when it fails. The Codex provider supports macOS and
138
+ Linux; it is refused on Windows (use Claude there).
139
+
140
+ ### Changed
141
+
142
+ - Each heartbeat now sends email after the agents that read and answer
143
+ replies have finished, and native mail providers (Gmail/SMTP) read the
144
+ mailbox every cycle, so a reply always cancels its queued follow-ups before
145
+ that cycle's sends (previously the two could run at the same time, and a
146
+ reply arriving with no open conversation waited for a later cycle). This
147
+ applies to Claude and Codex alike.
148
+ - A reply is only recorded once its reply draft (if it needs one) is
149
+ written, so a brain outage part-way never half-records it. If a working
150
+ brain returns no usable draft, the conversation is flagged for a human
151
+ instead of being left open with no answer.
152
+ - Usage is tracked per provider. The budget gate, `franco usage`, `franco
153
+ status` and the dashboard read and label the active provider's own quota;
154
+ existing usage history is kept and attributed to Claude.
155
+
156
+ ### Fixed
157
+
158
+ - Dashboard HTML and text assets now use UTF-8 explicitly, fixing Windows
159
+ decoding failures. Regression tests cover HTML, CSS, JavaScript and SVG.
160
+
161
+ ## [0.2.1] — 2026-10-13
162
+
163
+ A security release. Update if you run Franco.
164
+
165
+ ### Security
166
+
167
+ - **Claude now runs with no tools.** Every Claude call used to run with
168
+ `--dangerously-skip-permissions`, and prompts include text Franco doesn't
169
+ control: prospect email replies and scraped web pages. Instructions hidden
170
+ in that text could make Claude run commands, read or write files, or use
171
+ the operator's connected MCP servers. Calls now run with `--tools ""` and
172
+ `--strict-mcp-config`; Franco only ever needed text back, so nothing else
173
+ changes.
174
+ If you ran an earlier version with live replies coming in, check the
175
+ account that ran Franco for unexpected files or activity.
176
+
177
+ ### Fixed
178
+
179
+ - `franco run` no longer drops into the setup wizard when the trained config
180
+ lives in `franco.local.yaml`; the dashboard reads it too.
181
+ - A heartbeat cycle is capped at 10 minutes, so one hung agent can't freeze
182
+ Franco, and quiet hours are re-checked every 5 minutes so a Mac waking from
183
+ sleep resumes on time.
184
+ - Sites that only answer on plain HTTP or `www.` are no longer marked
185
+ unreachable.
186
+ - Profile and contact backfills rotate through every company instead of
187
+ retrying the same few forever.
188
+ - The trainer writes full place names ("Solon, Ohio") and real job titles, so
189
+ discovery doesn't geocode to the wrong city and found contacts aren't
190
+ silently discarded.
191
+
192
+ ### Added
193
+
194
+ - Contacts are backfilled for companies found without any, from their own team
195
+ pages and, if a key is set, Hunter's named-people search.
196
+ - `HUNTER_MONTHLY_SEARCHES` (default 25) sets one Hunter budget shared by every
197
+ domain search.
198
+
199
+ ## [0.2.0] — 2026-09-13
200
+
201
+ The release that made Franco worth running. The previous version could write
202
+ and send email but could not reliably find anyone to send it to, and it needed
203
+ a paid third-party platform to send at all. Both are fixed.
204
+
205
+ ### Added
206
+
207
+ - **A prospecting pipeline that works.** Four stages, each independently
208
+ runnable and re-runnable: DISCOVER (who exists) → PROFILE (what they are) →
209
+ ENRICH (who decides) → VERIFY (can you reach them).
210
+ - **DISCOVER, with a provider menu.** Four sources behind one adapter
211
+ interface, shown side by side in the dashboard with what each does, what it
212
+ costs, its free tier and which keys it needs. The default — OpenStreetMap —
213
+ needs no account, no card and no key, so the whole pipeline can be run
214
+ before paying anyone. `franco discover`.
215
+ - **Cost discipline in the collector.** Every discovery run estimates before it
216
+ spends, refuses to start over your cap, re-checks the cap between batches,
217
+ reads a kill switch, and flushes observations per batch so a run that dies
218
+ keeps what it learned. `franco discover --estimate` prints the projection and
219
+ exits.
220
+ - **PROFILE.** Free — three HTTP requests per business, no browser, no model
221
+ call. Reads the incumbent agency out of the site footer, ad pixels, missing
222
+ schema, abandoned blogs, open roles and named people. Chained onto DISCOVER
223
+ and run on every heartbeat. `franco profile`.
224
+ - **A signal catalog you confirm.** Franco knows how to collect 23 signals and
225
+ collects none of them until a human says so. Each shows a plain-language
226
+ description, what it costs, and how many companies already carry it.
227
+ `franco signals`.
228
+ - **The cohort builder.** Pick what a prospect must have and what disqualifies
229
+ them; see the matching companies live. A prospect list is a query you can
230
+ explain, not a file.
231
+ - **Native mail providers.** Gmail (REST + OAuth) and any SMTP+IMAP mailbox.
232
+ - **The outbox approval ladder.** Every outgoing email waits for approval by
233
+ default (`pending_review → approved → sent`), reviewed one at a time in a
234
+ decisions desk with `A`/`R`/`J`/`K`. A deterministic pre-send gate — no model
235
+ involved — rejects unrendered merge tags, banned phrases, over-length bodies,
236
+ too many links, HTML, undeliverable addresses and recipient mismatches.
237
+ - **Bounce handling and a kill switch.** A bounce marks the address invalid,
238
+ cancels that prospect's queued sends, and trips a global pause past a
239
+ configured bounce rate. `franco sending pause|resume`.
240
+ - **Claude usage tracking.** Per-call attribution by agent, task and model, plus
241
+ live subscription-quota gauges read the same way `/usage` does, so Franco
242
+ throttles itself and leaves headroom for your own interactive work.
243
+ `franco usage`.
244
+ - **CSV export.** `franco export` writes a sequencer-ready list. Franco is worth
245
+ running as a list-builder even if you never let it send.
246
+ - **A dashboard worth looking at.** Opens on Today — what needs a human — with
247
+ an activity feed and a rail of pipeline figures, collector runs and setup.
248
+ Light and dark are the same design re-tokenised; appearance follows your OS by
249
+ default. Fonts are vendored so it renders with the network off.
250
+ - **A Python version guard.** `python3 -m venv` on macOS builds from the system
251
+ 3.9, which cannot run Franco. It now fails immediately with the fix instead of
252
+ surfacing later as an unrelated-looking ImportError.
253
+
254
+ ### Changed
255
+
256
+ - **The data model.** Every fact Franco learns is now an observation — a row in
257
+ `observations`, never a column. A new signal needs no migration, re-observing
258
+ over time is a free time series, and confidence and provenance travel with the
259
+ fact. The vocabulary is governed: a database trigger rejects any signal code
260
+ not in `signal_codes`.
261
+ - **Email finding is pattern-first.** Franco derives the domain's address format
262
+ and verifies one candidate rather than brute-forcing name variations, and
263
+ grades every address honestly as `verified` / `risky` / `guess` / `invalid`.
264
+ Only deliverable addresses are ever sent to.
265
+ - **The default mail provider** is now `gmail`. Instantly is still supported as
266
+ a legacy option.
267
+ - **Discovery depth defaults to 30, not 100.** Google removed
268
+ 100-results-per-page in September 2025, so depth 100 is now billed as ten
269
+ pages nearly everywhere — and below rank 30 it is mostly directories.
270
+ - **The dashboard's HTML, CSS and JS** are real files in `franco/web/` instead
271
+ of a 1,500-line string literal in `dashboard.py`. Still no build step.
272
+
273
+ ### Fixed
274
+
275
+ - **Guessed emails were being reported as verified** whenever the domain merely
276
+ had an MX record. The migration downgrades every legacy `email_verified=1` row
277
+ to `guess`.
278
+ - **Cohorts counted the wrong companies.** PROFILE records a false boolean when
279
+ it checks and finds nothing — which is correct — but `cohort()` matched on the
280
+ row existing, so "companies running Google Ads" silently meant "companies we
281
+ checked for Google Ads", which was all of them. Cohorts now read the newest
282
+ observation per company and signal, and require it to be true.
283
+ - **Usage tracking counted every Claude session on the machine**, not just
284
+ Franco's. The transcript reconciler is gone; the ledger contains only rows
285
+ Franco's own Brain wrote.
286
+ - **Page titles were being extracted as people** ("Request Service", "Commercial
287
+ Services"), and titles bled between team members.
288
+ - **Confirming a signal before anything had loaded the catalog** reported success
289
+ while changing nothing.
290
+ - **The free OSM provider answered unmapped search terms with every registered
291
+ office in the area** — coffee roasters and churches for a roofing query. It now
292
+ refuses the query and names the trades it can search.
293
+
294
+ ### Security
295
+
296
+ - Test fixtures no longer carry real businesses' phone numbers or street
297
+ addresses.
298
+ - `franco.local.yaml` (gitignored) overrides the tracked `franco.yaml`, so a
299
+ fork can hold real product configuration without it reaching a public repo.
300
+
301
+ ## [0.1.0]
302
+
303
+ Initial release: the heartbeat loop, five sub-agents, the skills library, the
304
+ website trainer, Instantly-based sending, and the first dashboard.
franco-0.5.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gortex
4
+ Copyright (c) 2026 Ethan Rogers
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # The sdist must carry what setup.py bundles into the wheel.
2
+ include franco.yaml .env.example LICENSE README.md CHANGELOG.md requirements.in requirements.txt
3
+ recursive-include prompts *.md
4
+ recursive-include skills *.md
5
+ exclude skills/product_knowledge.md skills/competitive_intel.md
6
+ recursive-include franco/web *
7
+ global-exclude __pycache__ *.py[cod] .DS_Store