research-loop
Orchestrates survey-architect, feedback-synthesizer, and research-reporter as an iterative research loop that routes back upstream when a downstream step can't proceed cleanly, with per-study state an
SKILL.mdux-research/research-loop/SKILL.md
GitHubResearch 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).
detect_entry_point is the cold-start path — it infers progress by
checking which files exist, for a study that has no state file yet
(including one that predates this state model). Once 00-state.json
exists for a study, prefer reading it (below) over re-sniffing files on
every turn.
File structure
/research/<study-name>/
00-intake.md — product, learning goal, instrument decision (survey-architect)
00-intake.json — same facts, canonical/machine-readable (survey-architect)
00-state.json — explicit state + loopback count + artifact validation (research-loop)
01-survey-spec.json — deploy-ready spec: items, scoring, n, triggers (survey-architect)
02-synthesis.md — scores, CIs, significance, themes, severity (feedback-synthesizer)
02-synthesis.json — same facts, canonical/machine-readable (feedback-synthesizer)
03-report-internal.md — internal-audience report (research-reporter)
03-report.json — same facts, canonical/machine-readable (research-reporter)
03-report-formal.docx — formal-audience report, if requested (research-reporter)
/research/_benchmarks/<product>.md
— running log: date, instrument, score, CI, n, measurement conditions, per wave
— running qual theme taxonomy for the product
research_root (/research) and benchmarks_root
(/research/_benchmarks) come from ../config/product-context.yaml —
study_dir()/benchmark_path() default to those same values, but the
config file is the place to change them, not this file or the scripts.
For every .md/.json pair above, the .json is canonical — it's
what the next skill reads and what state/validation logic checks. The
.md is the human-readable copy of the same facts, never a place where
additional information lives that the .json doesn't have.
Every skill reads what exists before assuming it needs to build it. If
00-intake.md/.json already answers a question a skill would
otherwise ask the researcher, use it — don't re-ask.
State
00-state.json shape: study_id, state (one of routing.STATES:
INTAKE, SURVEY_DESIGN, DATA_READY, SYNTHESIS,
SYNTHESIS_REVIEW, REPORTING, REPORT_REVIEW, COMPLETE),
loopbacks ({total, max}), artifacts (artifact name → path),
validation (artifact name → "valid" or the problem list a
validate_* function returned).
Read/write it through the script, not by hand:
routing.read_state(study_dir_path)—Noneif the study hasn't started tracking state yet (fall back todetect_entry_point).routing.next_state(current_state, target_state)— validates the transition against a fixed graph before it happens; raises (naming the actually-allowed next states) on an invalid jump, e.g.INTAKEstraight toREPORTING. Allowed kickback edges:SYNTHESIS → SURVEY_DESIGN,SYNTHESIS_REVIEW → SYNTHESIS,REPORT_REVIEW → SYNTHESIS.routing.write_state(study_dir_path, state)— persists it.
This is deliberately a flat file plus a small fixed graph, not a
workflow engine — its only job is to catch an accidental stage-skip and
to replace re-sniffing files on every turn with reading one file. It
doesn't replace judgment about when a stage is actually done; a skill
still decides that before calling next_state.
Print this at every handoff so the researcher can see where things stand
(the state block below can be derived directly from 00-state.json):
━━ Research Loop State ━━━━━━━━━━━━━━━━━━━━
Study: [study-name]
Product: [product, from intake]
Learning goal: [one line, from intake]
State: [current STATES value]
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 if routing.check_benchmark_comparability
says comparable — otherwise "not comparable: <mismatched
fields>", waves reported side by side instead of a trend]
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
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Rules
Priority when rules interact: STOP > MUST NOT > MUST >
SHOULD > MAY — see ux-research/README.md for the full
precedence explanation (that file is the one place it's spelled out;
every skill's Rules section just tags bullets with these keywords).
- STOP before a third loop-back —
should_escalate_to_researcherreturningTruemeans end processing and surface the trade-off (options A/B below), not attempt the loop-back anyway. - MUST NOT force standalone feedback (no survey behind it) through survey-architect first.
- MUST NOT treat 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.
- MUST NOT call
next_statewith a jump the transition graph doesn't allow, and MUST NOT advance state without running the validator for the artifact that stage produced. - MUST NOT let research-reporter fill in a missing CI or severity tier itself instead of kicking back to synthesizer.
- MUST NOT imply a benchmark trend when
check_benchmark_comparabilityreports a mismatch — report the waves side by side instead. - MUST write to
/research/_benchmarks/<product>.mdafter a completed synthesis. - SHOULD skip re-running survey-architect's intake when
00-intake.md/.jsonalready 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 |
| Two waves, measurement conditions differ | Report side by side, not as a trend |
| Loop complete | Completion block, update benchmark file |
Maintaining this skill
evals/ is the regression suite. After touching scripts/routing.py, run
python3 -m unittest discover -s evals -v (CI runs the same). After
touching this file's prose, re-check the cases in
evals/qualitative_cases.md by hand.