ARCHITECTURE

A graph, not an agent.

Springy is a deterministic pipeline. Every run takes the same path: opportunity → anchor → critique loop → human gate → parallel fan-out → critique-per-platform → human gate → draft writer → publish queue → API-or-browser publisher. No LLM is deciding which tool to call next. That's the entire design.

springy cascade — the graph
                    ┌─ Discovery ─┐
                    │ RSS, briefings, competitor scrapes
                    │ ↓
                    RANKED OPPORTUNITY  (scored, persisted)
                           │
                           ▼
                    ANCHOR ESSAY  (generator + critique loop ×3)
                           │
                     ╔═════╧═════╗
                     ║  HITL #1← you approve / revise / abort
                     ╚═════╤═════╝
                           │
          ┌────────┬───────┼────────┬────────┐
          ▼        ▼       ▼        ▼        ▼
       LinkedIn  Thread  Bluesky   Reel    TikTok  
       (each: critique loop → humanize — in parallel)
                           │
                     ╔═════╧═════╗
                     ║  HITL #2← you pick which platforms ship
                     ╚═════╤═════╝
                           │
                    DRAFT WRITER  (writes to content/drafts/)
                           │
                           ▼
                    PUBLISH QUEUE  (SQLite, durable)
                           │
                           ▼
                    API PUBLISHERBROWSER FALLBACK
                    (on auth / rate-limit errors)
                           │
                           ▼
                    METRICS COLLECTOR  (1h / 4h / 24h / 7d polls)
                           │
                           ▼
                    feeds back into voice-RAG + variant scoring

Why a graph beats an agent.

Agents are great for exploration. They're not great at "do the same thing reliably every day, flag anything weird, never post something off-brand."

A graph is auditable: you can trace every decision back to a specific node. It's testable: each node is a pure function over the previous output. It's debuggable: the JSONL trace file records every step. And it's cheap: no extra tool-call round trips.

Springy does use LLMs inside each node — the critic is Claude Haiku or GPT-5-mini, the generator is Claude Sonnet 4.5 or GPT-5 — but the node boundaries are code. The LLM never picks what happens next.

The five nodes that do the work.

Discovery + Ranker

Reads data/briefings/*.md, RSS feeds, and optional competitor scrapes. Ranks candidates by pillar fit, recency, and perceived novelty. Persists to the opportunities_ranked table.

Anchor + Critique Loop

Generates the long-form essay, then a critic scores it on 8 dimensions (9 for long-form, with a "vulnerability" check). Low score → revise with feedback, re-score, up to 3 iterations. A CRITIQUE_BUDGET_USD cap prevents runaway spend.

Parallel Fan-out

LinkedIn, X thread, Bluesky, Threads, Reel, TikTok, Medium, Dev.to — each platform gets its own critique loop that prioritizes that platform's native conventions. All run in parallel. Brand voice retrieved once up front, shared.

Queue + Publisher

Atomic claim (BEGIN IMMEDIATE) so two drainers never publish the same row twice. API first, Playwright browser fallback on auth / rate-limit errors. Max 3 attempts with 2 → 4 → 8-minute backoff; then dead-letter for manual review.

The voice layer.

The differentiator vs. a generic LLM wrapper is the brand-voice RAG. On voice:seed, springy embeds your approved past posts with Voyage-3 into a local LanceDB store. On every generation, it retrieves the 5 closest past examples for the current platform + pillar and injects them as few-shot context.

You're not writing in GPT-voice. You're extending your own voice. Even on a cold corpus (5-10 examples), the output shifts noticeably. The corpus grows every time you approve a new post.

Voice-RAG is strongly recommended but optional. Without it, the generator works from brand.yaml rules alone — usable, less distinctive. Springy will warn you loudly on startup if you haven't seeded.

What's stored where.

Path Contains Safe to delete?
config/brand.yamlVoice rules, pillars, handles, audience.Back it up first.
config/.*-tokens.jsonOAuth / API tokens per platform (0o600).Yes — re-auth.
data/analytics.dbQueue, posts, metrics, variants, costs.Loses history.
data/voice-rag/LanceDB vectors of your past posts.Yes — re-seed.
data/traces/*.jsonlOne-line-per-event cascade trace.Yes. Auto-pruned >90 days.
content/drafts/Generated platform-specific drafts.Yes, if already published.

Trust boundaries.

Everything runs on your laptop by default. The only network traffic is to (a) your LLM provider, (b) optional Voyage / Replicate / ElevenLabs, (c) the platforms you publish to. Telemetry is opt-in and has an aggressive sanitizer — no content, no paths, no token-shaped strings.

Plugins under plugins/ execute as trusted code. springy plugins:list reads a static plugin.json manifest only, so inventory is safe. Actual loading happens only when a cascade uses the plugin.

See SECURITY.md for the full threat model.