# Focus Group

> Clone your customers from real sales-call transcripts, then pitch them anything. Discovers 3-5 customer archetypes in your transcript library, builds each into a grounded composite persona, and convenes them as a standing focus group that reacts to any offer, hook, price, page, or email before it goes live.

- Skill: `bradautomates/focus-group` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add bradautomates/focus-group`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bradautomates/focus-group/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- License: MIT
- Author: bradautomates (https://skillmd.com/u/bradautomates)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bradautomates/focus-group

---


# /focus-group

Clone your ideal customers from real call transcripts, then run them as a standing focus group. Setup runs once: it discovers the 3-5 customer archetypes hiding in the user's transcript library and builds each into a composite persona dossier. After that, every `/focus-group <stimulus>` convenes the panel to react to whatever the user drops in.

**The product is disagreement.** This is a moderated panel, not an LLM council: nothing here ranks responses, picks a winner, or synthesizes a single answer. The readout reports the distribution — who buys, who doesn't, and why.

## Resolve `SKILL_DIR` (do this before any command)

Every `python3 ...` command below runs `SKILL_DIR/scripts/setup.py`, a direct sibling of this file. Set `SKILL_DIR` to the absolute path of the directory containing THIS SKILL.md you just Read (e.g. Read `~/.codex/skills/focus-group/SKILL.md` → `SKILL_DIR=~/.codex/skills/focus-group`), and substitute it for `${SKILL_DIR}` below. On Windows use `python` instead of `python3`.

## Where everything lives

All artifacts live in a user-visible folder, never inside the skill:

```
~/Documents/FocusGroup/            # or SAVE_DIR from the .env
  business.md                      # ICP + business context (the panel's CLAUDE.md)
  panel/
    juno/dossier.md                # modules + quote bank + coverage map
    juno/positions.md              # appended every session
    dana/...
  sessions/
    2026-07-06-pricing-page.md     # stimulus, reactions, splits, verdict
  transcripts/                     # local cache of calls pulled via MCP
  inventory.json                   # transcript index + cluster assignments
```

Config lives in `~/.config/focus-group/.env` (transcript source + `SETUP_COMPLETE` marker). **NEVER overwrite an existing `.env`** — read it first, append missing keys with `>>`, never `>`.

Because sessions are flat dated markdown, later runs (and other skills) can grep them: "what has the panel said about pricing" is a file read, not a rebuild.

## Step 0 — Setup preflight (runs every invocation, silent on success)

On the first `/focus-group` call in a session, run structured preflight:

```bash
python3 "${SKILL_DIR}/scripts/setup.py" --json
```

Branch on the output:

- **`can_proceed: true`** → a group exists. Proceed to Phase 2 (run the panel) without comment. If `stale_dossiers` is non-empty or `pending_transcripts >= 2`, mention it in ONE line after the panel readout and offer an incremental rebuild — staleness is a warning, never a blocker.
- **`first_run: true` or `status: "no_group"`** → genuine first-time setup. Run Phase 1 (the full wizard) below. If the user provided a stimulus with the command, preserve it — run the panel immediately after setup completes.
- **`status: "source_unreachable"`** → the configured transcript folder is missing or the key is gone. Panel runs still work on the existing group; rebuilds are blocked. Tell the user in one line, then proceed.

On follow-up calls in the same session, use the silent check instead:

```bash
python3 "${SKILL_DIR}/scripts/setup.py" --check
```

Exit 0 means run the panel — the script emits **nothing** on success. **Do NOT announce "setup is complete"** on normal runs; the only user-visible output from Step 0 is when remediation is required. Non-zero exits:

| Exit | Meaning | Action |
|------|---------|--------|
| `2` | No group built | Run Phase 1 wizard |
| `3` | Transcript source unreachable | One-line warning; panel still runs, rebuilds blocked |

## Phase 1 — Setup (runs once; incremental rebuilds on trigger)

Setup is three steps: get the transcripts, analyze them, confirm the panel with the user. The transcripts tell you who the customers are — don't interview the user up front; show them what you found and let them correct it.

### 1a. Get the transcripts

Run the builder to scaffold config + the group dir (idempotent, never clobbers):

```bash
python3 "${SKILL_DIR}/scripts/setup.py"
```

If it reports no transcript source, ask ONE question. **Recommend a local folder of downloaded transcripts** — file reads are free, while every document pulled through an MCP tool call lands in model context (setup libraries run hundreds of thousands of tokens). Use `AskUserQuestion`:

Question: "Where do your sales call transcripts live? A local folder of downloaded files is the cheapest and fastest option."
Options:
- "A local folder of transcripts (recommended)" — ask for the path, verify it exists and count the files, append `TRANSCRIPTS_DIR=<path>` to `~/.config/focus-group/.env`. Pause until they have a folder.
- "Via a connected MCP tool (Fireflies, Granola, Google Drive, ...)" — check whether that provider's tools are already available in this session; if not, point them at connecting it (Claude Code: `claude mcp add` or Settings → Connectors) and pause — you cannot authenticate for them. Once connected, append `TRANSCRIPT_MCP=<provider>` to the `.env`.

**MCP path only:** pull under strict token rules — list metadata first, cap at the newest **50** after warning that MCP pulls are token-expensive, fetch **one doc per call** writing each to `<SAVE_DIR>/transcripts/<date>-<slug>.md` immediately, 3-4 docs per subagent, never echo transcript text. The cache is permanent — rebuilds never re-pull.

### 1b. Analyze (subagents; transcripts enter context once, ever)

All reads come from local files (`TRANSCRIPTS_DIR` or `<SAVE_DIR>/transcripts/`). `.docx` files can't be Read directly — convert once first (`textutil -convert txt *.docx` on macOS; stdlib unzip of `word/document.xml` elsewhere), writing the `.txt` next to the originals.

Fan out extraction subagents, **3-4 transcripts each**. Each agent Reads its files once and returns **only compact JSON** (≲1k tokens per transcript): who, date, call type, outcome (closed / stalled / lost / in progress), role and business type, problem language (**their words**), objections, price reactions, decision process (who else weighs in, how fast), tone markers, what made them lean in or check out, plus quote-bank candidate lines. **Never re-emit transcript text** — no verbatim rewrites, no long quotes in scratch files; the transcript folder IS the archive. Everything downstream works from these JSON extractions, never raw transcripts.

Then cluster the real people into archetypes **by decision behavior, not demographics** — two founders who both stall on "let me talk to my ops person" belong together even if one runs a gym and one runs an agency. Rules:
- Every archetype needs **3+ real people** behind it and a real behavioral split from its neighbors (different objections, buying speed, price sensitivity).
- If the library only supports 2 clean archetypes, present 2. **Never pad with a thin one.** Flag outliers that fit nowhere; don't force them into a seat.
- Write `inventory.json` — the transcript index plus cluster assignments (the `file` field must be the transcript's filename; preflight uses it to count pending transcripts):
   ```json
   {"built": "2026-07-06", "transcripts": [
     {"file": "2026-05-12-acme-demo.txt", "who": "Jordan (Acme)", "date": "2026-05-12",
      "type": "demo", "outcome": "closed", "cluster": "juno"}]}
   ```

### 1c. Confirm with the user, then build

Present what the transcripts revealed, then let the user shape it — this is the ONLY required conversation in setup:

> From your N calls I found these archetypes:
> 1. **Juno** — solo founder, moves fast, stalls on price (7 people, 12 calls)
> 2. **Dana** — ops lead, needs the boss's sign-off, price-insensitive but slow (5 people, 9 calls)
> ...
> And this is who your customer appears to be: <one-paragraph ICP drafted from the closed/won patterns>
> Which seats do you want? You can rename, merge, or cut.

Fold in their corrections ("Dana-types are actually bad-fit — we stopped selling to them"), then:

1. Write `business.md` — the drafted ICP plus the user's corrections: what they sell, who it's for, which deal patterns fit and which don't. This is the panel's standing context; sessions update it over time (with user approval).
2. For each **confirmed** seat only, compile `panel/<name>/dossier.md` from its cluster's extractions:

| Module | Contents |
|--------|----------|
| Data inventory | Which transcripts feed this composite; coverage gaps flagged |
| Identity | Role, business, stage, who influences their decisions |
| Buying journey | First touch → objection → close/stall timeline |
| Objection map | Objections raised, price reactions, what resolved or killed each |
| Style guide | Voice: pacing, skepticism, phrases, what makes them lean in |
| Quote bank | 20-30 verbatim-ish lines that anchor the clone's voice |
| Coverage map | Topics the dossier can speak to vs. blind spots (e.g. "no pricing data") |

   Also create an empty `panel/<name>/positions.md` with a `# Positions — <Name>` header.

   **Composites, never real people 1:1.** Seats are fictional blends (Juno, Dana), never a named real client — the 3+-people rule enforces this structurally. Real names appear only in `inventory.json` and the data-inventory module, never in a dossier's identity.

3. Mark setup done:

```bash
python3 "${SKILL_DIR}/scripts/setup.py" --mark-complete
```

## Phase 2 — Run (the focus group)

The user drops in a stimulus: an offer, hook, price, landing page, email, positioning line. Pattern is a **moderated panel**. Cost per standard run: N + 1 + contested seats + 1 subagent calls (~6-8 with a 4-seat panel) — cheap enough to spam with ideas.

### Round 1: independent reactions

Dispatch N Task subagents **in parallel, one per seat**, each loaded with ONLY:
- That seat's `dossier.md`
- The relevant entries from that seat's `positions.md` (prior positions on related topics — grep for keyword overlap with the stimulus)
- The stimulus, verbatim
- The reaction protocol below

Isolation is the point: personas must not see each other's dossiers or reactions in Round 1.

**Reaction protocol (include in every clone's prompt):**

You are <Name>, a composite of real people whose words are in your dossier. React in two layers, in order:

1. **Pre-verbal signal** (emit first, as three fields):
   - `attention`: did this land at all? (none / skim / hooked)
   - `valence`: drawn in or put off? (-2..+2)
   - `arousal`: do you care? (low / medium / high)
   Ground each in what the dossier says moves you. **If attention is `none` or `skim`, your verbal reaction must be short and dismissive — one or two lines, the way you'd actually scroll past — not a thoughtful critique.**
2. **Verbal reaction**: reason from that signal into your spoken take, objections, and decision (buy / maybe / no / not-for-me).

Rules:
- **Null reactions are allowed and valuable.** "I'd have scrolled past this" is a complete answer.
- **No-data flag:** if nothing in your dossier covers this topic (check the coverage map), say `NO-DATA` and stop. Do not improvise an opinion.
- **Receipts rule:** every objection or enthusiasm must cite dossier evidence — a quote-bank line or timeline event — or be explicitly tagged `[extrapolation]`.
- **Voice:** react in the patterns the quote bank shows, not generic AI-customer talk. A persona with an opinion about everything is the tell of a fake panel — don't coax fuller answers out of a clone that shrugged.

### Moderator pass

You (or one subagent) read Round 1 and extract the 2-3 sharpest splits — points where seats genuinely diverge (Juno buys at that price, Dana doesn't). The moderator is a plain synthesizer, not a persona: it adds **zero** opinions, only identifies where the disagreement is.

### Round 2: targeted cross-talk

Only the clones party to a split get called again — one subagent per contested seat, shown **just the opposing take** (not the whole round), with this anti-convergence instruction:

> Hold your position unless the argument would genuinely move someone with your history. If you move, say exactly what moved you. Either way, state what WOULD change your mind.

Position flips are recorded as high-value events in the session file. **Open all-see-all cross-talk is banned** — LLM sycophancy turns it into artificial consensus.

### Synthesis (the readout)

Report the distribution. Never resolve it. Format:

```markdown
## Panel readout: <stimulus, one line>

**Headline:** SHIP / KILL / CHANGE — <one sentence; say plainly when the panel was split>

**Unanimous:** <points every seat agreed on, if any>

**Splits:**
- <point>: Juno (buy — "quote/receipt") vs Dana (no — "quote/receipt")

**Top objections:** <each with seat attribution and receipt>

**What would flip each holdout:** <from Round 2 answers>

**Null / no-data:** <seats that scrolled past or had no coverage — this is signal, not filler>
```

### Persistence (do this every run, before the live room)

1. Write `sessions/{YYYY-MM-DD}-{stimulus-slug}.md`: the stimulus verbatim, each seat's two-layer reaction, the splits, Round 2 exchanges, and the readout.
2. Append each seat's position to its `panel/<name>/positions.md`: date, stimulus slug, stance, one-line why. These load into the clone next session ("three weeks ago you said $3k felt steep") — without this the same clone contradicts itself across sessions and accumulated verdicts mean nothing.

### Live room

Stay in the session. The user can grill any clone ("Dana, what if it's half price?") — dispatch that one seat again with its dossier + this session's context. Append anything learned to the session file so the artifact stays complete.

`business.md` accumulates too: when sessions repeatedly surface the same verdict pattern ("Dana-types never buy the top tier"), propose an edit to the ICP section and get the user's approval before writing it.

## Rebuild triggers

Re-run setup **incrementally** (never from scratch) when `pending_transcripts >= 2` for an existing archetype (refresh that dossier), 5+ new transcripts don't fit any archetype (propose a new seat, don't silently add), or a dossier is older than 60 days (`stale_dossiers`). `/focus-group rebuild` runs 1b again over new transcripts only, updates `inventory.json`, and re-presents the roster if seats changed.

## Security & Permissions

Reads transcripts via a user-connected MCP server or local folder, caching under `<SAVE_DIR>/transcripts/`. Writes personas, sessions, and an index to `~/Documents/FocusGroup/` (or `SAVE_DIR`) as plain markdown/JSON, plus config to `~/.config/focus-group/.env` (mode `0600`). Never sends data externally beyond the user's own MCP pulls, never connects MCP servers itself, never overwrites existing files (fills gaps and appends only), never simulates a real named individual (every seat is a 3+-person composite). Bundled script: `scripts/setup.py` — review before first use.

