[object Object]

← back to Claude Webdev Accelerator

bounce: intake engine (waitlist -> engagement + gated reply) wiring the funnel to the accelerator pipeline

e9af9ddb95273c2bf40f34aafa3260e8d94bece4 · 2026-08-11 12:41:46 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit e9af9ddb95273c2bf40f34aafa3260e8d94bece4
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Tue Aug 11 12:41:46 2026 -0700

    bounce: intake engine (waitlist -> engagement + gated reply) wiring the funnel to the accelerator pipeline
---
 .gitignore               |   3 +
 scripts/bounce-intake.js | 288 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 291 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e8bf25e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+
+# bounce-intake local state (per-machine, not shared)
+clients/.intake-processed.json
diff --git a/scripts/bounce-intake.js b/scripts/bounce-intake.js
new file mode 100644
index 0000000..bd50434
--- /dev/null
+++ b/scripts/bounce-intake.js
@@ -0,0 +1,288 @@
+#!/usr/bin/env node
+// bounce-intake.js — the connective tissue between the Bounce landing funnel and
+// the Web-Dev Accelerator pipeline.
+//
+// Reads the bounce-studio waitlist (data/waitlist.jsonl), finds leads that have
+// NOT yet been triaged, and for a chosen lead:
+//   1. scaffolds a client engagement folder under clients/<slug>/ with a BRIEF.md
+//      seed shaped for ACCELERATOR.md Phase 0 (Intake),
+//   2. drafts a scoped, human-voiced reply into ~/.claude/yolo-queue/pending-approval/
+//      (SEND IS GATED — this script NEVER emails anyone),
+//   3. records the lead as processed so it is never double-handled.
+//
+// $0 (local): pure Node builtins, no network, no paid API. The bounce-intake AGENT
+// runs this to get its work list, then does the human judgement (reading the
+// reference link, writing the real brief) on top of the scaffold.
+//
+// Usage:
+//   node scripts/bounce-intake.js list                 # show new (untriaged) leads
+//   node scripts/bounce-intake.js list --all           # show every lead, triaged or not
+//   node scripts/bounce-intake.js scaffold <n|email>   # triage lead #n (from list) or by email
+//   node scripts/bounce-intake.js scaffold-all         # triage every new lead
+//   node scripts/bounce-intake.js show <n|email>       # print one lead's raw record
+
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const HOME = os.homedir();
+const ACCEL = path.join(HOME, 'Projects', 'claude-webdev-accelerator');
+const WAITLIST = path.join(HOME, 'Projects', 'bounce-studio', 'data', 'waitlist.jsonl');
+const CLIENTS = path.join(ACCEL, 'clients');
+const PROCESSED = path.join(CLIENTS, '.intake-processed.json');
+const PENDING = path.join(HOME, '.claude', 'yolo-queue', 'pending-approval');
+
+// ---------- helpers ----------
+function readLeads() {
+  if (!fs.existsSync(WAITLIST)) return [];
+  const out = [];
+  const lines = fs.readFileSync(WAITLIST, 'utf8').split('\n');
+  for (const line of lines) {
+    const s = line.trim();
+    if (!s) continue;
+    try { out.push(JSON.parse(s)); } catch (_) { /* skip malformed line */ }
+  }
+  return out;
+}
+
+function leadId(lead) {
+  // Dedupe on EMAIL ALONE, not timestamp+email. A double-click / retry submit
+  // produces two different ms-precise timestamps for the same person, so a
+  // ts-keyed id would double-scaffold them (two client folders, two gated
+  // replies to hand-clean). Email-only absorbs the retry. Trade-off: a genuine
+  // re-engagement from the same email later needs a manual marker reset
+  // (delete its entry in clients/.intake-processed.json), which is fine for a
+  // Steve-reviewed funnel.
+  return (lead.email || '').toLowerCase().trim();
+}
+
+function readProcessed() {
+  if (!fs.existsSync(PROCESSED)) return {};
+  try { return JSON.parse(fs.readFileSync(PROCESSED, 'utf8')); } catch (_) { return {}; }
+}
+function writeProcessed(map) {
+  if (!fs.existsSync(CLIENTS)) fs.mkdirSync(CLIENTS, { recursive: true });
+  fs.writeFileSync(PROCESSED, JSON.stringify(map, null, 2) + '\n');
+}
+
+// Collapse untrusted free text to a single safe line before embedding it in a
+// markdown BRIEF/reply — kills newline-spliced fake sections (a note of
+// "foo\n\n## Injected" would otherwise render as a real BRIEF heading) and strips
+// leading markdown structural chars.
+function oneLine(s) {
+  return (s || '').toString().replace(/\s+/g, ' ').replace(/^[#>*\-\s]+/, '').trim();
+}
+
+// Pull the first URL out of the ref field or the free-text note. Only accepts a
+// real dot-domain http(s) URL — rejects junk like "https://javascript:alert(1)"
+// that the server's bare-domain normalization can otherwise manufacture.
+function extractRef(lead) {
+  const clean = (u) => (/^https?:\/\/[^\s/.]+\.[^\s]{2,}/i.test((u || '').trim()) ? u.trim() : '');
+  if (lead.ref) { const r = clean(lead.ref); if (r) return r; }
+  const m = (lead.note || '').match(/https?:\/\/[^\s)]+/i);
+  return m ? clean(m[0]) : '';
+}
+
+function slugFor(lead, taken) {
+  const email = (lead.email || '').toLowerCase();
+  const domain = (email.split('@')[1] || 'client').split('.')[0];
+  let base = domain.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'client';
+  let slug = base, n = 2;
+  while (taken.has(slug)) { slug = `${base}-${n++}`; }
+  taken.add(slug);
+  return slug;
+}
+
+function fmtDate(iso) {
+  const d = iso ? new Date(iso) : new Date('2026-01-01T00:00:00Z');
+  return d.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
+}
+
+// ---------- brief + reply templates ----------
+function briefSeed(lead, slug, ref) {
+  const ts = lead.ts || '';
+  return `# Bounce engagement — ${slug}
+
+<!-- Seeded by scripts/bounce-intake.js from a Bounce waitlist lead. -->
+<!-- This is a SEED for ACCELERATOR.md Phase 0 (Intake). The bounce-build agent -->
+<!-- fills the real spec after reading the reference below. -->
+
+## Lead
+- **Email:** ${lead.email || '(none)'}
+- **Submitted:** ${fmtDate(ts)}  (\`${ts}\`)
+- **What they're building:** ${oneLine(lead.budget) || '(not specified)'}
+- **Reference link:** ${ref || '(none provided — ask for one)'}
+- **Their note:** ${oneLine(lead.note) || '(none)'}
+
+## Phase 0 · Intake — brief (fill this in)
+- **Client / brand:**
+- **Outcome / "high value":**  <!-- revenue, deal won, time saved -->
+- **What we build:**  <!-- one paragraph, scoped -->
+- **Hard constraints:**  <!-- brand, domain, deadline, budget = $15k flat -->
+- **Differentiation vector:**  <!-- from /site-audit + /competitors on the ref link -->
+
+## Six-skill plan (Bounce marketing name → real skill)
+1. **brief**      → /site-audit + /competitors on the ref → scoped BRIEF (this file)
+2. **design**     → /mockups <ref> 3 · four-horsemen · graphic-designer critique
+3. **front-end**  → frontend-design / vp-engineering build from the locked brief
+4. **components** → bento-grid · rotating-hero-page · page-flip-ui · modal-rig (drop-in)
+5. **motion**     → motion-graphics / hyperframes-animation (scroll reveals, fades)
+6. **media**      → media-use · room-setting-generator · canvas-design (on-brand imagery)
+
+## Status
+- [x] Lead captured + engagement scaffolded (bounce-intake)
+- [ ] Reference audited, brief locked (Phase 0/1)
+- [ ] Scaffold + build (Phase 2/3)
+- [ ] /5x verify + /contrarian gate (Phase 4)
+- [ ] Preview behind basic auth for Steve (Phase 5)
+- [ ] STOP — launch/deploy/DNS gated (Phase 6)
+`;
+}
+
+function replyDraft(lead, slug, ref) {
+  const first = (lead.email || '').split('@')[0].replace(/[._-].*$/, '');
+  // First name only, title case (CLAUDE.md welcome-email rule): upcase first
+  // char, downcase the rest so ALLCAPS aliases (INFO@, SALES@) → "Info"/"Sales"
+  // rather than "hey INFO,".
+  const firstTitle = first ? first.charAt(0).toUpperCase() + first.slice(1).toLowerCase() : 'there';
+  const buildLine = oneLine(lead.budget) || 'your site';
+  const refLine = ref
+    ? `i've already pulled up ${ref} and started the reference teardown.`
+    : `if you've got a site or a look you love, drop the link and i'll start the teardown.`;
+  return `# GATED — Bounce lead reply draft · ${slug}
+
+**Action for Steve:** review + send (or edit). Nothing has been sent. The lead is
+already scaffolded at \`clients/${slug}/BRIEF.md\` in claude-webdev-accelerator.
+
+- **To:** ${lead.email || '(no email)'}
+- **Re:** ${buildLine}
+- **Reference:** ${ref || '(none provided)'}
+- **Submitted:** ${fmtDate(lead.ts)}
+
+---
+
+**Subject:** your Bounce build — scoped brief inside
+
+hey ${firstTitle},
+
+thanks for sending this over. ${refLine}
+
+here's how it goes from here: i come back with a one-paragraph scoped brief —
+the palette, type, and motion language locked — before a single line of layout
+exists. then the six skills build it: front-end turns the brief straight into
+clean code, design + components handle the polished pages and the animated
+pieces, motion runs the scroll reveals, and media generates the on-brand imagery.
+about a week, brief to launch-ready. $15k flat — you know the number before we
+start and it doesn't move.
+
+want me to lock the brief and give you a start date?
+
+— bounce /idc
+
+---
+
+*Draft only. Send is gated (no outbound mail from the funnel). Delete this file
+after sending, or move to a 'sent' folder.*
+`;
+}
+
+// ---------- commands ----------
+function newLeads() {
+  const leads = readLeads();
+  const done = readProcessed();
+  return leads
+    .map((lead, i) => ({ lead, i }))
+    .filter(({ lead }) => !done[leadId(lead)]);
+}
+
+function cmdList(showAll) {
+  const leads = readLeads();
+  const done = readProcessed();
+  if (!leads.length) { console.log('No leads yet in', WAITLIST); return; }
+  const rows = showAll ? leads : leads.filter((l) => !done[leadId(l)]);
+  if (!rows.length) { console.log('No new leads — all', leads.length, 'triaged. (use --all to see them.)'); return; }
+  console.log(`${rows.length} ${showAll ? 'total' : 'new'} lead(s):\n`);
+  rows.forEach((lead) => {
+    const idx = leads.indexOf(lead) + 1;
+    const flag = done[leadId(lead)] ? '✓' : '•';
+    const ref = extractRef(lead);
+    console.log(`  ${flag} #${idx}  ${lead.email || '(no email)'}  [${lead.budget || '—'}]  ${fmtDate(lead.ts)}`);
+    if (ref) console.log(`        ref: ${ref}`);
+    if (lead.note) console.log(`        note: ${String(lead.note).slice(0, 120)}`);
+  });
+  console.log('\nTriage one: node scripts/bounce-intake.js scaffold <n|email>');
+}
+
+function resolveLead(sel) {
+  const leads = readLeads();
+  if (/^\d+$/.test(sel)) return leads[parseInt(sel, 10) - 1] || null;
+  const s = sel.toLowerCase();
+  return leads.find((l) => (l.email || '').toLowerCase() === s) || null;
+}
+
+function scaffoldLead(lead, takenSlugs) {
+  const done = readProcessed();
+  if (done[leadId(lead)]) {
+    console.log(`Already triaged: ${lead.email} → ${done[leadId(lead)].slug}`);
+    return null;
+  }
+  const ref = extractRef(lead);
+  const slug = slugFor(lead, takenSlugs);
+  const dir = path.join(CLIENTS, slug);
+  fs.mkdirSync(dir, { recursive: true });
+  fs.writeFileSync(path.join(dir, 'BRIEF.md'), briefSeed(lead, slug, ref));
+
+  if (!fs.existsSync(PENDING)) fs.mkdirSync(PENDING, { recursive: true });
+  const replyPath = path.join(PENDING, `bounce-lead-${slug}.md`);
+  fs.writeFileSync(replyPath, replyDraft(lead, slug, ref));
+
+  done[leadId(lead)] = { slug, at: new Date().toISOString(), reply: replyPath };
+  writeProcessed(done);
+
+  console.log(`✓ ${lead.email} → clients/${slug}/BRIEF.md`);
+  console.log(`  reply draft (GATED): ${replyPath}`);
+  return slug;
+}
+
+function cmdScaffold(sel) {
+  const lead = resolveLead(sel);
+  if (!lead) { console.error('No lead matches', JSON.stringify(sel)); process.exit(1); }
+  const taken = new Set(fs.existsSync(CLIENTS) ? fs.readdirSync(CLIENTS).filter((f) => !f.startsWith('.')) : []);
+  const slug = scaffoldLead(lead, taken);
+  if (slug) {
+    console.log('\nNext: hand it to the build agent →  clients/' + slug + '/BRIEF.md');
+    console.log('      (bounce-build reads the ref, locks the brief, runs the pipeline.)');
+  }
+}
+
+function cmdScaffoldAll() {
+  const fresh = newLeads();
+  if (!fresh.length) { console.log('No new leads to triage.'); return; }
+  const taken = new Set(fs.existsSync(CLIENTS) ? fs.readdirSync(CLIENTS).filter((f) => !f.startsWith('.')) : []);
+  let n = 0;
+  for (const { lead } of fresh) { if (scaffoldLead(lead, taken)) n++; }
+  console.log(`\nTriaged ${n} new lead(s).`);
+}
+
+function cmdShow(sel) {
+  const lead = resolveLead(sel);
+  if (!lead) { console.error('No lead matches', JSON.stringify(sel)); process.exit(1); }
+  console.log(JSON.stringify(lead, null, 2));
+}
+
+// ---------- main ----------
+const [cmd, ...rest] = process.argv.slice(2);
+switch (cmd) {
+  case 'list': cmdList(rest.includes('--all')); break;
+  case 'scaffold': if (!rest[0]) { console.error('usage: scaffold <n|email>'); process.exit(1); } cmdScaffold(rest[0]); break;
+  case 'scaffold-all': cmdScaffoldAll(); break;
+  case 'show': if (!rest[0]) { console.error('usage: show <n|email>'); process.exit(1); } cmdShow(rest[0]); break;
+  default:
+    console.log('bounce-intake — waitlist → accelerator engagement\n');
+    console.log('  list [--all]            show new (or all) leads');
+    console.log('  scaffold <n|email>      triage one lead → clients/<slug>/BRIEF.md + gated reply draft');
+    console.log('  scaffold-all            triage every new lead');
+    console.log('  show <n|email>          print a lead record');
+    console.log('\nwaitlist:', WAITLIST);
+}

← cf67d4a accelerator: model-arena live via tunnel (Steve-approved dep  ·  back to Claude Webdev Accelerator  ·  chore: session-close hardening — atomic writeProcessed, (no 0f1dbb8 →