
Survey Generator
- 50 installs
- 2.8k repo stars
- Updated August 3, 2026
- rohitg00/pro-workflow
Helps with ai & agent building tasks during AI-assisted development.
About
survey-generator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- survey-generator
- AI & Agent Building
- AI-coding skill
Survey Generator by the numbers
- 50 all-time installs (skills.sh)
- +10 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #7,278 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rohitg00/pro-workflow --skill survey-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 50 |
|---|---|
| repo stars | ★ 2.8k |
| Last updated | August 3, 2026 |
| Repository | rohitg00/pro-workflow ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Survey Generator
Provider-agnostic literature-survey artifact generator. Output flows into a pro-workflow wiki, not a standalone HTML file — survives sessions and indexes for FTS5 retrieval.
Diff vs dair-academy version
| dair | pro-workflow |
|---|---|
| Hardcoded Kimi K2.6 on Fireworks | Provider-agnostic (Anthropic/OpenAI/OpenRouter/Fireworks/custom) |
| Output = single-file HTML with inline SVG | Output = wiki markdown page + bibliography rows in sources.md |
| One-off artifact, no follow-up | Persists in FTS5 index; reused by wiki-research-loop |
| Manual run only | Composable with /wiki research for auto-bibliography expansion |
When to use
- "Survey on <topic>" / "lit review on <topic>"
- Onboarding a new domain — generate the map-of-the-field
- After a wiki has 10-30 sources, compile a synthesis page over them
- Pre-step before
/wiki researchruns: gives the loop a high-quality seed bundle
Inputs
| Input | Required | Description |
|---|---|---|
topic | yes | "Reasoning Models", "Agentic Engineering" |
source_url | yes | Public anchor: arXiv survey, GitHub awesome-list, canonical blog post |
--wiki <slug> | yes | Target wiki for the artifact |
--bibliography-size N | no | Default 20. 40-50 comprehensive, 80-100 exhaustive |
--section-count N | no | Default 6-10 numbered sections |
--provider name | no | Override provider (default: first env var found) |
--model id | no | Override model |
Workflow (the agent runs these in order)
Step 1 — Read the anchor
WebFetch source_url. Extract subtopics + cited papers. For GitHub awesome-lists, walk README + linked papers files. For arXiv survey PDFs, use abstract + ToC.
Step 2 — Build research_bundle.json
Use templates/research_bundle.template.json as scaffold. Required keys:
{
"topic": "...",
"anchor_source": "...",
"abstract_hints": ["..."],
"taxonomy": [{"branch": "...", "children": [{"name": "...", "description": "..."}]}],
"sections": [{"title": "...", "guidance": "...", "papers": ["key1","key2"]}],
"bibliography": [{"key": "author-year-shortname", "authors": "...", "year": 2024, "title": "...", "venue": "...", "summary": "..."}]
}Hard rules:
- Every paper in
bibliographymust be real. No invented entries. - Every
keyreferenced insections[].papersmust exist inbibliography. - 4-8 taxonomy branches, 2-4 children each.
- 6-10 numbered sections covering: introduction → foundations → methods → evaluation → open problems.
Step 3 — Run the generator
node $SKILL_ROOT/scripts/build-survey.js \
--bundle <path-to-research_bundle.json> \
--wiki <slug> \
[--provider anthropic|openai|openrouter|fireworks|custom] \
[--model <id>]Generator: 1. Reads bundle. 2. Sends to LLM with strict markdown spec (numbered sections, inline [^paper-key] citations, no HTML). 3. Writes output to <wiki>/derived/surveys/<topic-slug>.md. 4. Appends bibliography rows to <wiki>/sources.md (deduped by key). 5. Calls wiki-cli.js page to upsert into FTS5 index.
Step 4 — Iterate
If prose is thin: tighten sections[].guidance and rerun. Output filename versions automatically (<slug>-v2.md, <slug>-v3.md).
To compare providers:
node build-survey.js --bundle bundle.json --wiki agent-memory --provider openai --model gpt-4o
node build-survey.js --bundle bundle.json --wiki agent-memory --provider anthropic --model claude-opus-4-7Each writes a separate versioned file; diff them.
Output structure
<wiki-root>/
├── sources.md # bibliography rows appended (deduped)
└── derived/surveys/
└── <topic-slug>-v1.md # the survey
# title (h1)
# ## 1. Introduction
# ## 2. Foundations
# ...
# ## References
# [^src-bib-<slug>] author year. title. venue.Hard rules
1. Never invent bibliography entries — every paper must be a real work with venue. 2. Every section's papers array references keys in bibliography. 3. Output is markdown ONLY. No HTML, no inline SVG, no JS. 4. Bibliography rows in sources.md use the slug-style id src-bib-<slug> (derived from the bibliography key); cite as [^src-bib-<slug>]. Manual non-bibliography sources continue to use src-NNN. 5. Iterate on inputs (research_bundle.json), not on the generated output. 6. Provider+model selection is the user's call — never hardcode.
Composing with research loop
/wiki init reasoning-models --title "Reasoning Models" --flavor research
# Manually compile a research_bundle.json
node skills/survey-generator/scripts/build-survey.js --bundle bundle.json --wiki reasoning-models
# Now the wiki has a structured survey + 50 bibliography rows
# Enable auto-research to expand:
# (edit reasoning-models/wiki.config.md, set auto_research.enabled: true)
node skills/wiki-research-loop/scripts/research-loop.js seed reasoning-models "chain-of-thought failure modes" --depth 0
node skills/wiki-research-loop/scripts/research-loop.js run reasoning-models#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const https = require('https');
const { execFileSync } = require('child_process');
const PRO_WORKFLOW_ROOT = path.resolve(__dirname, '..', '..', '..');
const COUNCIL = path.join(PRO_WORKFLOW_ROOT, 'skills', 'llm-council', 'scripts', 'council.js');
function parseArgs(argv) {
const out = { _: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith('--')) {
const key = a.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith('--')) { out[key] = next; i++; }
else out[key] = true;
} else out._.push(a);
}
return out;
}
function die(msg) { console.error(`[survey] ${msg}`); process.exit(1); }
function getStore() {
const distPath = path.join(PRO_WORKFLOW_ROOT, 'dist', 'db', 'store.js');
if (!fs.existsSync(distPath)) die(`built store missing at ${distPath}. Run npm run build`);
return require(distPath).createStore();
}
function postJSON(urlStr, body, headers, timeoutMs = 180000) {
return new Promise((resolve, reject) => {
const url = new URL(urlStr);
const data = JSON.stringify(body);
const req = https.request({
hostname: url.hostname,
path: url.pathname + url.search,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data), ...headers },
}, res => {
let chunks = '';
res.on('data', c => { chunks += c; });
res.on('end', () => resolve({ status: res.statusCode, body: chunks }));
});
req.setTimeout(timeoutMs, () => req.destroy(new Error('survey request timeout')));
req.on('error', reject);
req.write(data);
req.end();
});
}
const PROVIDER_DEFAULTS = {
anthropic: { envKey: 'ANTHROPIC_API_KEY', baseUrl: 'https://api.anthropic.com', model: 'claude-opus-4-7' },
openai: { envKey: 'OPENAI_API_KEY', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o' },
openrouter: { envKey: 'OPENROUTER_API_KEY', baseUrl: 'https://openrouter.ai/api/v1', model: 'anthropic/claude-opus-4' },
fireworks: { envKey: 'FIREWORKS_API_KEY', baseUrl: 'https://api.fireworks.ai/inference/v1', model: 'accounts/fireworks/models/kimi-k2p5' },
custom: { envKey: 'LLM_COUNCIL_API_KEY', baseUrl: process.env.LLM_COUNCIL_BASE_URL || '', model: process.env.LLM_COUNCIL_CHAIRMAN || '' },
};
function pickProvider(arg) {
if (arg && PROVIDER_DEFAULTS[arg]) return arg;
for (const [name, p] of Object.entries(PROVIDER_DEFAULTS)) if (process.env[p.envKey]) return name;
return null;
}
async function callProvider(providerName, model, system, user, maxTokens) {
const p = PROVIDER_DEFAULTS[providerName];
if (!process.env[p.envKey]) die(`${p.envKey} not set`);
if (providerName === 'anthropic') {
const res = await postJSON(`${p.baseUrl}/v1/messages`, {
model, max_tokens: maxTokens, system, messages: [{ role: 'user', content: user }],
}, { 'x-api-key': process.env[p.envKey], 'anthropic-version': '2023-06-01' });
if (res.status >= 400) die(`anthropic error ${res.status}: ${res.body.slice(0, 300)}`);
const data = JSON.parse(res.body);
return (data.content || []).map(b => b.text || '').join('');
}
const res = await postJSON(`${p.baseUrl}/chat/completions`, {
model, max_tokens: maxTokens, temperature: 0.7,
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
}, { Authorization: `Bearer ${process.env[p.envKey]}` });
if (res.status >= 400) die(`${providerName} error ${res.status}: ${res.body.slice(0, 300)}`);
const data = JSON.parse(res.body);
return data.choices?.[0]?.message?.content || '';
}
function slugify(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60); }
function bibCitationId(key) {
return `src-bib-${slugify(key)}`;
}
function appendBibliographyToSources(wikiRoot, bibliography) {
const file = path.join(wikiRoot, 'sources.md');
let existing = '';
if (fs.existsSync(file)) existing = fs.readFileSync(file, 'utf8');
const seenKeys = new Set();
for (const m of existing.matchAll(/\| (src-bib-[a-z0-9-]+) \|/g)) seenKeys.add(m[1]);
const newRows = [];
for (const b of bibliography) {
const id = bibCitationId(b.key);
if (seenKeys.has(id)) continue;
const url = b.url || (b.venue && b.venue.startsWith('arXiv:') ? `https://arxiv.org/abs/${b.venue.slice(6)}` : '');
newRows.push(`| ${id} | paper | ${url} | ${b.title.replace(/\|/g, '\\|')} | ${b.key} | ${new Date().toISOString().slice(0, 10)} |`);
}
if (!newRows.length) return 0;
const tableHeader = '| id | type | url | title | key | added_at |\n| --- | --- | --- | --- | --- | --- |';
const hasHeader = existing.includes('| id | type |');
if (!hasHeader) {
const prefix = existing.length ? (existing.endsWith('\n') ? existing : existing + '\n') : '';
fs.writeFileSync(file, `${prefix}${tableHeader}\n${newRows.join('\n')}\n`);
} else {
fs.writeFileSync(file, existing.trimEnd() + '\n' + newRows.join('\n') + '\n');
}
return newRows.length;
}
function nextVersion(dir, baseSlug) {
if (!fs.existsSync(dir)) return 1;
const re = new RegExp(`^${baseSlug}-v(\\d+)\\.md$`);
let max = 0;
for (const f of fs.readdirSync(dir)) {
const m = f.match(re);
if (m) max = Math.max(max, parseInt(m[1], 10));
}
return max + 1;
}
function buildPrompt(bundle) {
const bibWithIds = bundle.bibliography.map(b => ({ ...b, citation_id: bibCitationId(b.key) }));
const sectionsWithIds = bundle.sections.map(s => ({
...s,
paper_citation_ids: (s.papers || []).map(k => bibCitationId(k)),
}));
return `Compile a literature survey on the topic "${bundle.topic}" using ONLY the bibliography provided.
Output strict markdown:
- H1 = topic title
- Numbered H2 sections following the provided sections list
- Inline citations as [^citation_id] using the EXACT citation_id from the bibliography below (e.g., [^src-bib-park-2023-generative-agents])
- A "## References" section at the end listing every cited [^citation_id] with: citation_id, authors, year, title, venue, one-sentence summary
- No HTML, no SVG, no inline images
- ~600-1200 words per section, scaled by bibliography size
- For each section, weave together the papers in section.paper_citation_ids; do not just list them
Bibliography (USE THE citation_id FIELD EXACTLY for inline citations):
${JSON.stringify(bibWithIds, null, 2)}
Sections to produce in order (use paper_citation_ids for citations):
${JSON.stringify(sectionsWithIds, null, 2)}
Anchor (context only, do not cite):
${bundle.anchor_source || ''}
Hard rules:
- Cite real papers from the bibliography only. Do not invent.
- Every section that lists papers MUST cite each one at least once via its citation_id.
- Use [^citation_id] for inline citations. The References section reuses these citation_id values.
- Do not write any prose under the H1; start sections immediately.`;
}
async function cmdRun(args) {
const bundlePath = args.bundle;
const slug = args.wiki;
if (!bundlePath || !slug) die('usage: build-survey.js --bundle <path> --wiki <slug> [--provider name] [--model id]');
if (!fs.existsSync(bundlePath)) die(`bundle not found: ${bundlePath}`);
const bundle = JSON.parse(fs.readFileSync(bundlePath, 'utf8'));
if (!bundle.topic || !Array.isArray(bundle.bibliography)) die('bundle missing topic or bibliography[]');
const invalid = bundle.bibliography.find(
b => !b || typeof b.key !== 'string' || !b.key.trim() || typeof b.title !== 'string' || !b.title.trim()
);
if (invalid) die('bundle bibliography[] entries must include non-empty string key and title');
const bibKeys = new Set();
for (const b of bundle.bibliography) {
if (bibKeys.has(b.key)) die(`duplicate bibliography key: ${b.key}`);
bibKeys.add(b.key);
}
if (Array.isArray(bundle.sections)) {
for (const [i, s] of bundle.sections.entries()) {
if (!Array.isArray(s.papers)) continue;
for (const k of s.papers) {
if (typeof k !== 'string' || !k.trim()) die(`sections[${i}].papers contains non-string entry`);
if (!bibKeys.has(k)) die(`sections[${i}].papers references unknown bibliography key: ${k}`);
}
}
}
const providerName = pickProvider(args.provider);
if (!providerName) die('no provider env var set');
const model = args.model || PROVIDER_DEFAULTS[providerName].model;
if (!model) die('no model — pass --model');
const store = getStore();
let wiki;
try { wiki = store.getWiki(slug); } finally { store.close(); }
if (!wiki) die(`unknown wiki: ${slug}`);
console.error(`[survey] generating with ${providerName}:${model} for wiki ${slug}`);
const md = await callProvider(providerName, model, 'You are a careful technical-writing assistant generating a literature survey.', buildPrompt(bundle), 16000);
const surveysDir = path.join(wiki.root_path, 'derived', 'surveys');
fs.mkdirSync(surveysDir, { recursive: true });
const baseSlug = slugify(bundle.topic);
const v = nextVersion(surveysDir, baseSlug);
const fileName = `${baseSlug}-v${v}.md`;
const fileAbs = path.join(surveysDir, fileName);
fs.writeFileSync(fileAbs, md);
const added = appendBibliographyToSources(wiki.root_path, bundle.bibliography);
console.error(`[survey] wrote ${fileAbs}`);
console.error(`[survey] appended ${added} new bibliography rows to sources.md`);
// Index via wiki-cli
const wikiCli = path.join(PRO_WORKFLOW_ROOT, 'skills', 'wiki-builder', 'scripts', 'wiki-cli.js');
const relPath = path.relative(wiki.root_path, fileAbs);
try {
execFileSync('node', [wikiCli, 'page', slug, relPath, '--type', 'survey'], { stdio: 'inherit' });
} catch (e) {
die(`wiki-cli page failed: ${e.message}`);
}
console.log(JSON.stringify({ slug, file: fileAbs, version: v, bibliography_added: added }, null, 2));
}
async function main() {
const [, , ...rest] = process.argv;
const args = parseArgs(rest);
if (rest.length === 0 || args.help) {
console.error('Usage: build-survey.js --bundle <path> --wiki <slug> [--provider anthropic|openai|openrouter|fireworks|custom] [--model id]');
process.exit(1);
}
await cmdRun(args);
}
main().catch(e => { console.error(e); process.exit(1); });
{
"topic": "<concise survey topic, e.g. 'Reasoning Models'>",
"anchor_source": "<URL of the public anchor: arXiv survey, awesome-list, canonical blog post>",
"abstract_hints": [
"<one bullet on the core motivation>",
"<one bullet on the key contributions to highlight>",
"<one bullet on the open questions to surface in the conclusion>"
],
"taxonomy": [
{
"branch": "<top-level category 1>",
"description": "<one sentence>",
"children": [
{"name": "<sub-area>", "description": "<one sentence>"},
{"name": "<sub-area>", "description": "<one sentence>"}
]
}
],
"sections": [
{
"n": 1,
"title": "Introduction",
"guidance": "Frame the topic, state why it matters now, preview the taxonomy.",
"papers": []
},
{
"n": 2,
"title": "Foundations",
"guidance": "Cover the prerequisite concepts and earliest-cited papers.",
"papers": ["author1-year-key", "author2-year-key"]
},
{
"n": 3,
"title": "Methods",
"guidance": "Group method papers by taxonomy branch.",
"papers": []
},
{
"n": 4,
"title": "Evaluation",
"guidance": "Benchmarks, evaluation protocols, contested measurements.",
"papers": []
},
{
"n": 5,
"title": "Open Problems",
"guidance": "What remains unsolved, what the field disagrees on.",
"papers": []
}
],
"bibliography": [
{
"key": "author1-year-shortname",
"authors": "Last, F., Other, A.",
"year": 2024,
"title": "<paper title>",
"venue": "<conference/journal/arXiv:NNNN.NNNNN>",
"summary": "<one to two sentence summary of contribution>"
}
]
}