Adds blog_post campaign type that publishes directly to the CircuitForge
website via Directus CMS. Implements PostingStrategy ABC with:
- supports_dupe_guard() → False (blog posts run on demand, no weekly guard)
- execute() → wraps publish_blog_post() from directus.py
- tags stored as JSON string in DB, decoded at post time
Migration 017 adds slug, tags, seo_description columns to campaign_variants.
Poster merges variant blog fields into extra dict before strategy execution.
Seed script updated with blog campaign (id=9, target=blog/main).
Registry updated; 6 new unit tests added.
Adds max_posts INTEGER to campaign_subs (NULL = unlimited/evergreen).
Adds successful_post_count() query counting lifetime success records.
poster.py checks max_posts before the 7-day rolling dupe guard.
Root cause: campaign 2 fired 8 days after the last post (just outside the
7-day window), allowing a duplicate r/opensource pitch. Fix: set max_posts=1
on intro campaigns so the lifetime cap fires regardless of window.
FastAPI backend (SQLite + APScheduler), Vue 3 frontend, MCP server for
Claude integration, and Docker Compose stack. Includes campaign data model
(campaigns → variants → subs), post history, sub rules, and Playwright-based
Reddit posting layer migrated from claude-bridge/reddit-poster.
Also seeds legacy campaigns (6) and sub rules (14) from reddit-poster history.
Closes#1 (scaffold), resolves migration from claude-bridge.