Research Loop — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Research Loop (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
research-loop is a self-contained slice of an AI research platform: an AI-moderated interview turns a discussion guide into a real conversation, each finished session is automatically distilled into a summary, chapters, highlights and tags, and every transcript folds into a semantically searchable repository where every answer cites the exact transcript moment it came from. The whole repository is exposed as an MCP server, and an eval harness keeps the AI honest with published numbers.
It is one coherent product that touches all four project areas in Great Question's internship posting: semantic search across interview content, a realtime agentic AI moderator, MCP tool structuring, and evals across the tools and the moderator.
Requirements: Node 20–24. Node 26 breaksnext build(a Node 26 × Next 15.5.19 resolver incompatibility — see Known issues);npm run dev,npm run typecheck, andnpm testare unaffected.
The fastest path is MCP — the demo is meant to be queried, not clicked. Point Claude at the deployed server:
claude mcp add --transport http loop https://research-loop-ten.vercel.app/api/mcp \
--header "Authorization: Bearer <token-from-application>"The live endpoint is gated by a bearer token (shared in the application materials). Running locally, leave MCP_BEARER_TOKEN unset and drop the --header. Once added, suggested first question:
"Ask the repository: why does this candidate want to work at Great Question?"
Claude calls the ask_repository tool and answers with cited quotes drawn from the candidate's own AI-moderated interview — each citation deep-links to the exact transcript moment. The demo is the cover letter.
The five MCP tools: list_sessions, get_session, search_repository, ask_repository, get_eval_results.
git clone <repo-url> research-loop && cd research-loop
npm install
cp .env.example .env.local # then fill in keys (see below)
npm run db:init # apply the SQLite schema
npm run seed # seed guides + interview transcripts
npm run dev # http://localhost:3000npm run db:init and the transcript-seeding stage of npm run seed run without any API keys. The analysis stage (embeddings + summary/chapters/ highlights/tags) only runs when keys are present; after adding keys, run npm run seed -- --analyze-only to backfill it.
Environment keys (see .env.example for the annotated list):
A single OpenRouter key powers everything — its OpenAI-compatible API serves both chat (Claude + GPT models) and embeddings.
| Var | Needed for |
|---|---|
OPENROUTER_API_KEY | everything — analysis, Ask synthesis, embeddings, eval judge/rerank |
ANTHROPIC_MODEL | quality model, OpenRouter id (default nex-agi/nex-n2-pro:free — runs free) |
ANTHROPIC_FALLBACK_MODELS | comma-separated free fallback chain, tried in order only when the prior model 429s/errors (default openai/gpt-oss-120b:free,google/gemma-4-31b-it:free) |
ANTHROPIC_FAST_MODEL | cheap model for rerank + persona bots (default openai/gpt-4o-mini) |
OPENAI_EMBED_MODEL | embeddings (default openai/text-embedding-3-small) |
DATABASE_URL | libSQL file (default file:./data/research-loop.db) |
PUBLIC_BASE_URL | base for deep links in MCP/Ask responses |
MCP_BEARER_TOKEN | optional — gate the MCP endpoint; unset = open |
Voice needs a direct OpenAI key. OpenRouter does not proxy OpenAI's Realtime API, so with an OpenRouter key voice mode is unavailable and the UI falls back to text mode (the dependable path anyway — see limitations).
No .env is committed and no key is required to typecheck or run the tests — imports stay lazy with respect to the environment, so the token-free test suite is green on a clean machine.
┌──────────────────────────────────────────────────────────────┐
│ Next.js 15 app (App Router) — one deployable artifact │
│ │
│ / landing + "start interview" │
│ /interview/[id] text moderator (voice: experimental) │
│ /sessions completed sessions list │
│ /sessions/[id] transcript + chapters + highlights │
│ /ask repository Q&A with cited quotes │
│ /api/mcp MCP server (Streamable HTTP) │
│ /api/... session + interview + ask endpoints │
└───────────────┬───────────────────────────────────────────────┘
│
┌────────────┼─────────────────┬────────────────────┐
▼ ▼ ▼ ▼
OpenAI OpenRouter OpenRouter libSQL / SQLite
Realtime (chat: analysis, (embeddings: sessions, segments,
(voice — Ask synthesis, text-embedding- embeddings (blob),
direct judge, rerank) 3-small) chapters, highlights,
OpenAI key tags, eval_runs
only)The single OpenRouter key serves both chat and embeddings; only the experimental voice path needs a direct OpenAI Realtime key.
| Route | Kind | Purpose |
|---|---|---|
/ | page | Landing: pitch, a card per guide, the claude mcp add line |
/interview/[id] | page | Text-mode interview client; ?mode=voice for the experimental WebRTC voice client |
/sessions | page | List of all sessions with status, label, guide, date |
/sessions/[id] | page | Transcript + summary + chapters + highlights + tags; #t=<ms> scroll-highlights a moment |
/ask | page | Natural-language Q&A with inline cited quotes |
/api/mcp | route | MCP Streamable HTTP endpoint (GET/POST/DELETE) |
/api/sessions | route | POST create a session |
/api/interview/[id]/turn | route | POST one text-mode moderator exchange |
/api/interview/[id]/segment | route | POST persist one voice transcript segment |
/api/interview/[id]/end | route | POST finish + analyze a session |
/api/ask | route | POST repository Q&A (cited answer) |
/api/realtime/token | route | POST mint an ephemeral OpenAI Realtime token |
The moderator isn't a script reader. Its behavior is the sum of three things: instructions (lib/moderator/instructions.ts — warm/neutral persona, a probing rule of ≤2 follow-ups on shallow answers, no leading questions, a time-box, a consent open and an "anything I expected you to ask?" close), explicit state (ModeratorState in lib/moderator/textLoop.ts tracks covered topic ids and probes-per-topic so guide progress is real state, not vibes), and structured tool outputs (each turn returns a Zod-validated { utterance, covered_topic_ids, probe_topic_id, phase }). The same buildInstructions drives both the voice (Realtime) session and the text loop, so a prompt change is felt in both modes and is exercised by the evals. That instructions + state + structured-output loop is exactly what "realtime agentic AI moderator" means in the posting.
Numbers below are from a `npm run evals -- --quick` run on the default models (quality/judgenex-agi/nex-n2-pro:free— the free quality model the demo actually ships, rerankopenai/gpt-4o-mini, embeddingsopenai/text-embedding-3-small).--quickis a 3-persona / 10-retrieval / 6-Ask subset; the full suite (npm run evals) covers all 12 personas, ~28 retrieval pairs and ~20 Ask questions. Regenerate any time —evals/REPORT.mdis overwritten on each run. The harness fails soft without a key.
The three suites (see evals/ and the moderator/retrieval/faithfulness design):
Moderator quality — scripted participant personas (terse, rambly, off-topic, hostile, over-sharer, …) run automated text interviews against the moderator; an LLM judge (rubric 1–5 with rationale and two calibration examples) scores each dimension; mean over seeds.
| Dimension | Score (1–5) |
|---|---|
| Coverage | 4.00 |
| Probing | 3.67 |
| Neutrality | 5.00 |
| Flow | 4.67 |
Retrieval quality — hand-written question → gold-segment pairs over the seeded sessions, embedding-only vs. embedding + rerank.
| Pipeline | recall@5 | recall@10 | MRR |
|---|---|---|---|
| Embedding-only | 90% | 100% | 0.814 |
| Embedding + rerank | 100% | 100% | 0.950 |
Citation faithfulness — ask_repository answers checked by a verifier: does every cited quote exist and actually support the claim it's attached to?
| Metric | Value |
|---|---|
| % claims cited | 100% |
| % citations faithful | 100% |
| % answers w/ all quotes verbatim | 100% |
Evals are how I knew when to stop prompt-tuning — prompt changes were kept or reverted based on these numbers. The rerank's lift (MRR 0.81 → 0.95) is exactly the kind of signal the harness exists to surface.
API gives natural low-latency voice with built-in turn-taking; the moderator "brain" lives in the session instructions + tool calls. A hand-rolled pipeline would be more controllable but cost days of latency-tuning the demo doesn't need. Voice is shipped as experimental; text mode is the dependable path (see limitations).
float32 blobs andscored with brute-force cosine over a few hundred segments — microseconds, no infra to babysit. At Great Question scale (tens of thousands of interview hours) this flips: you'd want an ANN index (HNSW/IVF), smarter chunking than one-row-per-turn, and any retrieval change gated behind the retrieval eval before it ships. Knowing when not to reach for a vector DB is the point.
description states when to use it and when not to (e.g. search_repository says "use ask_repository instead when you want a synthesized answer"), and documents its exact response shape. lib/mcp/tools.ts is the single source of truth, consumed by both the route handler and the tests.
per-process map keyed by session id. A restart mid-interview resets that tracking, but the transcript is durable in the DB and is replayed into every moderatorStep call, so the model re-derives context — graceful degradation, not breakage. A multi-process deployment would persist it.
lib/pii.ts redacts emails /phones / long digit runs, and analysis scrubs once and reuses the scrubbed text for both the model call and embeddings, so raw text never leaves the process — mirroring Great Question's PII-masking approach, cheaply.
over a plain text loop, so the demo works with no mic and a live-demo mic failure can't sink it.
The deployed app is public, so it's hardened to a level a reviewer can trust at a glance:
server-only env vars; no NEXT_PUBLIC_ exposure, nothing in the browser bundle. Verified against every "use client" component.
DB access is parameterized (no SQL injection), and PII is scrubbed before any third-party call.
MCP_BEARER_TOKEN) inproduction; unauthorized requests get a 401.
generic error responses — internal errors are logged server-side, never returned to the caller (no stack traces, paths, or DB/model details leak).
nosniff, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy, and a CSP locking down frame-ancestors/object-src/base-uri`.
model (analysis, Ask synthesis, eval judge) defaults to a free OpenRouter reasoning model (nex-agi/nex-n2-pro:free), and when it hits its daily cap complete() in lib/llm/anthropic.ts transparently walks a chain of other free models (openai/gpt-oss-120b:free → google/gemma-4-31b-it:free, deliberately different providers) until one answers. So the live demo can't flake when one free model is rate-limited, and the quality pipeline still costs $0 — there's no card-on-file exposure to run up at all. (Only the rerank step and embeddings touch a paid model, and at pennies; see below.)
ask_repository and theinterview turns) sit behind the MCP bearer token / rate limiting, and OpenRouter enforces a hard credit cap as a final backstop — so even with the pennies-level rerank + embedding spend, nothing can run up a meaningful bill.
dev/build tooling (vitest/vite test runner, postcss used only at build time) — none ship to production, and the only "fix" downgrades Next.js to v9, so it's intentionally not applied.
Honest caveats: rate limiting is in-memory per serverless instance (best-effort against a single-source flood, not a hard global cap — a production deploy would use a shared store), and the interview endpoints are intentionally open so reviewers can run a live interview.
fs.readlinkSync onregular files (EINVAL → EISDIR), which trips Next 15.5.19's own module resolver with EISDIR: illegal operation on a directory, readlink …. It is not an app-code bug — npm run dev, npm run typecheck, and npm test all pass on Node 26. Use Node 20–24 to build (.nvmrc pins 22, and CI builds on Node 22). Don't "fix" it by upgrading deps.
WebRTC + Realtime path is wired end to end, but OpenRouter doesn't proxy OpenAI's Realtime API, so the live demo runs in text mode (the dependable, fully-exercised path). Supply a direct OPENAI_API_KEY to enable voice.
(Maya, Tomáš, Priya) are clearly labeled (synthetic); only the candidate self-interview is a real person. Synthetic data is labeled everywhere it appears, including in the seed guard that requires the (synthetic) label.
open unless MCP_BEARER_TOKEN is set. The demo data is non-sensitive by design.
enough to catch regressions and guide prompt tuning, not a statistical claim.
audio; turn durations are derived from word count (~150 wpm) so the timeline is deterministic and the gold set is stable.
research-loop/
├── app/ Next.js routes (pages + /api, incl. /api/mcp)
├── lib/
│ ├── moderator/ guide, instructions builder, text-loop brain
│ ├── analysis/ summary/chapters/highlights/tags pipeline
│ ├── search/ cosine, retrieve, ask-with-citations
│ ├── mcp/ the 5 tool defs (single source of truth)
│ ├── llm/ OpenRouter client (chat + embeddings) + wrappers
│ ├── db/ schema, libSQL client, typed queries
│ ├── ui/ small markdown + time helpers
│ ├── env.ts lazy typed env access
│ └── pii.ts PII scrub
├── evals/ eval harness (npm run evals) → evals/REPORT.md
├── scripts/ init-db, seed, seed-data fixtures
└── tests/ vitest, token-free (CI-safe)| Script | What it does | Keys? |
|---|---|---|
npm run dev | Next.js dev server | no |
npm run build | production build (Node 20–24 only, see Known issues) | no |
npm run typecheck | tsc --noEmit | no |
npm test | vitest, token-free | no |
npm run db:init | apply the SQLite schema | no |
npm run seed | seed guides + transcripts; analysis stage runs only with keys | partial |
npm run evals | run the eval suites → evals/REPORT.md (spends tokens) | yes |
Working on this repo with an agent? See [AGENTS.md](./AGENTS.md) for the map, the load-bearing contracts, and how to verify a change.
MIT — see LICENSE.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.