research-loop · v1.1.0 · 2026-09-12 · sha256 0d4f98555a4b7e7f

research-loop v1.1.0A

Immutable. This exact content is served forever at /api/v1/blob/0d4f98555a4b7e7f.

---
name: research-loop
version: 1.1.0
author: Personify Labs
description: >
  Orchestrates survey-architect, feedback-synthesizer, and research-reporter
  as an iterative research loop — not a one-way pipeline. Detects when a
  downstream skill can't proceed cleanly (data too thin to trust, wrong
  instrument for the stated goal) and routes back upstream instead of
  producing a shaky output. Maintains the per-study file structure at
  /research/<study-name>/ and the per-product longitudinal benchmark at
  /research/_benchmarks/<product>.md. Use this as the single entry point
  for any UX research task — a new study, raw data to make sense of, or a
  report to write — rather than calling the three skills directly. Trigger
  when someone starts a new research study, pastes raw feedback/survey
  data without a study already in motion, or asks to "run the research
  loop." Bundles scripts/routing.py for deterministic entry detection and
  the loop-back ceiling (run it, don't count loop-backs by hand) and
  evals/ for regression testing.
tags:
  - user-research
  - ux-research
  - orchestration
  - router
license: CC-BY 4.0
---

# Research Loop

Runs survey-architect, feedback-synthesizer, and research-reporter as a
loop that behaves like an actual research team: someone can say "that
sample's too small, we need more data" and send it back, rather than
quietly writing up a shaky number.

Each of the three skills works standalone. This skill exists for the
handoffs between them and the file structure that lets a later skill (or
a later *study*) pick up where an earlier one left off.

-----

## The loop at a glance

```
Entry detection
    ↓
survey-architect (intake → instrument → sample size → deploy-ready spec)
    ↓
 [ pause: wait for data collection ]
    ↓
feedback-synthesizer (score + CI + significance + theme + severity)
    ↑___________kick back if n insufficient for stated goal_______|
    ↓
research-reporter (audience-formatted output)
```

Max loop-backs: 2 per study. If a second kickback doesn't resolve it,
surface the trade-off to the researcher rather than looping a third time.

-----

## Entry detection

**Run `scripts/routing.py`'s `detect_entry_point(...)`** rather than
eyeballing the table below — pass whether a learning goal, raw data, an
existing study folder, and a synthesis file are present, and use its
return value. The table is here to show the reasoning, not to be
hand-matched each time:

| What's pasted / asked | Entry point |
|---|---|
| A research question, no data yet ("I need to know if X is usable") | survey-architect |
| Raw survey results / CSV / feedback pile, no study folder exists yet | feedback-synthesizer (standalone mode — no spec file to read) |
| Raw data + an existing `/research/<study-name>/` folder | feedback-synthesizer (loaded mode — reads the spec) |
| A finished `02-synthesis.md` and a request to write something up | research-reporter |
| "Run the research loop" with a vague ask | Start at survey-architect Step 0 (intake) — let intake clarify |

A finished synthesis file always wins the routing decision, even if a
learning goal or raw data is also present — it's the most-progressed
artifact for that study. Don't force every input through all three
stages: a pile of app store reviews with no survey behind it goes
straight to feedback-synthesizer in standalone mode, full stop — this
is a common failure mode worth guarding against explicitly (see
qualitative_cases.md Case 1).

-----

## File structure

```
/research/<study-name>/
  00-intake.md          — product, learning goal, instrument decision (survey-architect)
  01-survey-spec.json   — deploy-ready spec: items, scoring, n, triggers (survey-architect)
  02-synthesis.md        — scores, CIs, significance, themes, severity (feedback-synthesizer)
  03-report-internal.md  — internal-audience report (research-reporter)
  03-report-formal.docx  — formal-audience report, if requested (research-reporter)

/research/_benchmarks/<product>.md
  — running log: date, instrument, score, CI, n, per wave
  — running qual theme taxonomy for the product
```

Every skill reads what exists before assuming it needs to build it. If
`00-intake.md` already answers a question a skill would otherwise ask
the researcher, use it — don't re-ask.

-----

## Loop state

Print this at every handoff so the researcher can see where things stand:

```
━━ Research Loop State ━━━━━━━━━━━━━━━━━━━━
Study:         [study-name]
Product:       [product, from intake]
Learning goal: [one line, from intake]

Stage:
  ✓ survey-architect    instrument: [X], n required: [Y]
  ● feedback-synthesizer   in progress
  ○ research-reporter      pending

Kickbacks:     [none | count + reason]
Confidence:    [none flagged | LOW — n=X vs required Y]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

-----

## Kickback conditions

A kickback is any point where a downstream skill cannot honestly proceed
without an upstream decision changing.

**feedback-synthesizer → survey-architect:**
Sample size is so far below the required floor that the stated learning
goal genuinely can't be answered (not just "flag and proceed" — that's
the normal small-n handling inside synthesizer itself; this is the
escalated case where the gap is too large to paper over with a wider CI).
Kick back with: current n, required n, and a recommendation (extend
collection window, widen distribution, or accept a narrower claim than
originally scoped).

**feedback-synthesizer → survey-architect (instrument mismatch):**
The data doesn't actually support the learning goal the intake recorded
— e.g., a single NPS item was collected but the real question was about
task-level usability. Kick back with what instrument should have been
used, so the next wave corrects it.

**research-reporter → feedback-synthesizer:**
The synthesis file is missing something the report structure requires
(no severity tiers, no CI on a headline score) — this should be rare if
synthesizer followed its own spec, but the reporter should never
backfill or invent a missing CI/severity itself.

Output format for any kickback:

```
━━ Kickback ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
From:   [skill]
To:     [skill]
Reason: [one sentence]
What changes: [what the upstream skill should do differently this time]
Loop-back count: [N of 2 max]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

-----

## Max loop-backs

**Run `scripts/routing.py`'s `should_escalate_to_researcher(loopback_count)`**
before attempting another loop-back — don't just count in your head.
If it returns `should_escalate=True` (2 loop-backs already used):

> "Two loop-backs on [issue] without resolving it. Here's where things
> stand: [current state]. Options: A — accept the limitation and report
> it as a caveat, B — pause the study and revisit scope. Which do you
> want?"

Don't attempt a third loop-back without the researcher's input.

-----

## Modes

**Gated (default):** pause at each stage transition and each kickback
for confirmation. Use for a new study or an unfamiliar product.

**Fast:** run straight through, noting any kickbacks in the final
summary rather than pausing on them. Opt in with "fast mode" or "just
run it."

-----

## Loop completion block

```
━━ Research Loop Complete ━━━━━━━━━━━━━━━━━
Study:          [study-name]
Product:        [product]
Instrument:     [instrument used]
n:              [final n]  Confidence: [flagged / not flagged]

Headline result: [score + CI, one line]
vs. benchmark:   [trend direction, from _benchmarks file]

Top findings:    [Critical-tier items, one line each]
Kickbacks:       [none | list with reason]

Outputs:
  Internal report: [path]
  Formal report:   [path, if generated]
  Benchmark file:  updated at /research/_benchmarks/<product>.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

-----

## What this skill never does

- Forces standalone feedback (no survey behind it) through survey-architect first
- Treats a synthesizer small-n flag as an automatic kickback — that's
  normal handling inside synthesizer; only an unanswerable-goal gap
  escalates to a loop kickback
- Loops back more than twice without surfacing to the researcher
- Skips writing to `/research/_benchmarks/<product>.md` after a
  completed synthesis
- Lets research-reporter fill in a missing CI or severity tier itself
  instead of kicking back to synthesizer
- Re-runs survey-architect's intake if `00-intake.md` already answers
  the same questions

-----

## Quick reference

| Input | Entry point |
|---|---|
| New research question, no data | survey-architect |
| Raw data, no study folder | feedback-synthesizer (standalone) |
| Raw data, study folder exists | feedback-synthesizer (loaded) |
| Synthesis done, need a writeup | research-reporter |
| Synthesizer: n too low for the goal | Kick back to survey-architect |
| Synthesizer: wrong instrument for the goal | Kick back to survey-architect |
| Reporter: synthesis missing CI/severity | Kick back to feedback-synthesizer |
| 2 kickbacks, still unresolved | Surface to researcher, stop |
| Loop complete | Completion block, update benchmark file |