/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:
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. Ifstale_dossiersis non-empty orpending_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: trueorstatus: "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:
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):
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 addor Settings → Connectors) and pause — you cannot authenticate for them. Once connected, appendTRANSCRIPT_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 (thefilefield must be the transcript's filename; preflight uses it to count pending transcripts):{"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:
- Juno — solo founder, moves fast, stalls on price (7 people, 12 calls)
- 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:
- 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). - For each confirmed seat only, compile
panel/<name>/dossier.mdfrom 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.
- Mark setup done:
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 , a composite of real people whose words are in your dossier. React in two layers, in order:
- 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 isnoneorskim, your verbal reaction must be short and dismissive — one or two lines, the way you'd actually scroll past — not a thoughtful critique.
- 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-DATAand 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:
## 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)
- 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. - 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.