Capgo Builder — "Contact Support" & Easy Help

docs/superpowers/specs/2026-06-03-builder-contact-support-design.md · 2026-06-03
⏸ SHELVED — design complete, deemed too complex to build now (Martin). Preserved for reference.
Outcome: Fully designed & stress-tested (sizing, Cloudflare Email Service limits, gzip/truncation, telemetry, and integration with the Cap-go/automations email↔Discord bridge). The complexity concentrated in the bridge integration — making a programmatically-sent ticket behave correctly (avoiding the ownedOutboundCopy heuristic, the double-delivery race from multiple bridge-routed @capgo.app recipients, and two-party reply threading). The cleanest robust path (a dedicated POST /builder-ticket endpoint in the automations worker + team-in-Discord) was judged not worth the cost now. Preserved so the analysis isn't lost. No implementation plan was produced (§8 is the closest skeleton).

1 · Goal

Make it frictionless for a builder user to get help when something goes wrong, via two clearly-separated paths: A rich internal log is captured invisibly during every builder run and sent in full when help is requested — replacing today's truncated 12-line support bundle.

Scope: Capgo builder CLI flows only. Not the rest of the CLI; no standalone command.

2 · Triggers & menu

Unified help menu at three points. "Ask Capgo support" is always first.

TriggerBuild log?Menu options (in order)
Build failureYes (jobId + log)📨 Ask Capgo support🤖 Ask AI🔄 Try again❌ Exit
Onboarding errorNo📨 Ask Capgo support🔄 Try again / ↩️ Restart❌ Exit
Unhandled error (raw provider API error)No📨 Ask Capgo support🔄 Try again❌ Exit

AI is offered only when a build log exists. Onboarding/unhandled errors get support only.

AI ↔ support: fork + escalation

Independent choices. After AI returns its analysis and the user is still stuck, the AI result screen adds 📨 Still stuck — send this to Capgo support carrying the same logs + the AI analysis into the email (no re-gathering).

3 · Architecture & data flow

Reuses the proven path the AI feature already uses (CLI → ${apiHost}/build/* → builder worker). Backend is a thin proxy owning identity + rate limiting; the worker sends. Compression (gzip -9) happens in the CLI, so the worker is a near pass-through.

CLI (user machine)
  builder run writes a hidden verbose internal log as it goes
  on failure/error → unified help menu
  user picks "Ask Capgo support"
    • assemble ONE combined bundle (labeled sections):
        DIAGNOSTICS / INTERNAL CLI LOG / BUILD LOG (fastlane)? / AI ANALYSIS? / USER MESSAGE?
    • redact secrets (CLI-only) + TELL the user we do itGZIP -9 + BASE64 in the CLI (Node — no mem/CPU limits)
    • "We'll send your request to Capgo & email you a copy at <email>. Secrets removed."
        Send  /  View logs first  /  Cancel
        └ View logs first → Show path (copies to clipboard) / Open preview (combined bundle)
    • on Send → POST { appId, jobId?, bundleB64, … }  → ${apiHost}/build/support  (capgkey)
        │
        ▼
capgo-new backend  public/build/support.ts   (THIN PROXY, JSON only)
    1. auth capgkey → resolve user → VERIFIED email
    2. RATE LIMIT here (1/60s + 10/day per user) — NOT in the worker
    3. To = resolved email; reject if client-sent email differs
    4. forward JSON { bundleB64, verifiedEmail, appId, jobId? }
        ▼
capgo_builder worker  /support
    1. NO gzip, NO DO pull — drop bundleB64 into the attachment
    2. env.EMAIL.send(...)        ← Cloudflare Email Service binding
         from: "Capgo Builder Support" <capgo_builder_support@capgo.app>
         to:   <verified user email>            (the USER — clean, well-titled email)
         cc:   michael@capgo.app                (internal, NON-routed → safe; NOT martin@)
         bcc:  capgo_builder_support@capgo.app  (ONLY bridge-routed addr; invisible)
         (martin@ excluded — it IS bridge-routed → would race; NO replyTo)
         attachments: [{ content: bundleB64, filename: …log.gz, type: application/gzip }]
        ▼
Cap-go/automations bridge → ingests the BCC copy → R2 attachment → Discord [SUPPORT] thread
    (team works in Discord, not email)
    team replies → To = capgo_builder_support@ (routed → dropped as self-copy),
    CC = buildReplyAllCc = user + michael@ (originalTo+originalCc) → both get every reply → no loop/race

Split: CLI = capture + combined bundle + redact(+inform) + gzip-9/base64 · backend = identity + verified email + rate limit + forwarding · worker = attach blob + send.

4 · Email transport, addressing & real limits

Use Cloudflare Email Service (transactional, onboarded for capgo.app) — not the legacy Email Routing binding. Resend is not used. Capgo is on a paid CF plan (confirmed) — required to send to arbitrary recipients (the user).

Addressing — user is the visible To; BCC triggers the bridge

Invariant: exactly ONE bridge-routed recipient per email — the BCC'd capgo_builder_support@. To/CC may include non-routed addresses (the user; non-routed internal mailboxes like michael@), but never a second bridge-routed address (support@, martin@). Safety hinges on CC'd addresses staying non-routed — verify michael@ before shipping, never add a routed address to the CC config. Rare edge: if the builder user's own email is a routed @capgo.app address, the To copy also routes in → detect and drop the BCC in that case, or accept the dedup fallback.

Why a single routed address — the double-delivery race

Two bridge-routed recipients ⇒ the bridge receives the mail twice. Its Message-ID dedup (index.ts:605-621) is get-then-put, non-atomic, on eventually-consistent KV across separate worker invocations — so simultaneous double-delivery (e.g. a reply-all hitting two routed addresses) could slip through and double-post. One routed recipient (the BCC) makes the race impossible by construction.

Verified bridge behavior — replies reach the user (Cap-go/automations/email/index.ts)

Limits that actually matter (verified against Cloudflare docs)

LimitValueImplication
Total message size5 MiBHard ceiling on the base64'd email
… for verified destination addrs only25 MiBRecipient verified in the CF account (not the sender). The user isn't one → 5 MiB tier. Moot (measured 28 KB).
Attachmentsmust be base64 (~+33%)5 MiB budget is the encoded size
Worker memory128 MB / isolateNot a constraint — the "10 MB" is script bundle size
Email-binding CPU~50 ms / requestWhy we don't gzip in the worker
Recipients (to+cc+bcc)50Fine (we use 2)
Sizing model — MEASURED: a real GitHub build log (logs_72018286007.zip) = 123 KB raw → 28 KB gzip(-9)+base64 = ~4.4× shrink, 188× under the 5 MiB ceiling; the ~4.5 MiB encoded budget holds ~20 MiB raw. gzip level 9, run in the CLI.

Config & sender

// builder worker wrangler.jsonc (prod + preprod)
"send_email": [
  { "name": "EMAIL", "remote": true,
    "allowed_sender_addresses": ["capgo_builder_support@capgo.app"] }
]

Sender setup: the capgo.app domain onboarding (SPF/DKIM) is the sender verification — any @capgo.app address sends immediately.

5 · Internal log + combined bundle

Today only ~12 in-memory lines exist. Add a full, hidden, on-disk log per builder run.

The combined bundle

On Ask Capgo support, internal log + build log are combined into one bundle file with labeled sections (DIAGNOSTICS / INTERNAL CLI LOG / BUILD LOG (fastlane) / AI ANALYSIS / USER MESSAGE). One file = one attachment, one preview, one gzip. Built by extending the existing writeOnboardingSupportBundle, then gzipped(-9)+base64'd in the CLI.

6 · Support-flow UX (CLI)

User-facing email: addressed To: the user (CC'd to non-routed internal addrs like michael@); the Discord bridge is triggered by an invisible BCC. Preview is never shown by default — only via "View logs first".

7 · Security & privacy

8 · Components to build / change

CLI cli/src

capgo-new backend public/build

capgo_builder worker

Ops / DNS / account (one-time)

9 · Error handling & testing

10 · Open items + resolved decisions

Open

  1. Final menu/label + email subject/body wording (provisional).
  2. Confirm preprod Email Service onboarding for capgo.app.
  3. Optional: Discord role @mention when the bridge opens a builder ticket (small automations change; replaces the rejected email-CC idea that would have raced).

Resolved decisions