--- name: autonomous-research description: An 18-agent pipeline that turns one topic line into a drafted research paper, literature review, or thesis chapter. Finds real sources at Crossref and OpenAlex, drafts each section against them, attacks its own draft for weak claims, then compiles citations deterministically so in-text markers map one to one onto the bibliography and every printed DOI resolved at Crossref or DataCite. Every agent is a markdown file you can open and edit. Pure stdlib, no API key, no account. Use when someone asks to write a research paper, do a literature review, draft a thesis chapter, find sources on a topic and write them up, or names opendraft or openpaper. --- # Autonomous Research Turns one topic line into a finished paper with a real literature base. Ported from OpenDraft (github.com/federicodeponte/opendraft, MIT), which runs this as a hosted engine. Here the engine is you: eighteen stages and one evidence stage, each a prompt in `agents/`, plus the scripts that do the parts a language model must not do by hand. **You are the model.** No key, no service, no account. The only network calls are Crossref, OpenAlex and DataCite, all open endpoints. ## The one command ``` Write a paper on ``` Everything below follows from that. Do not ask the user to run the stages themselves; run them. ## Ask four things first Before stage 5, if the user has not already said, ask for: 1. **Target venue**, or "none, general academic". 2. **Total word limit.** 3. **Citation style**, one of the six in `references/citation-styles.md`. 4. **Document type**, one of the types in `references/paper-types.md`. These are four questions in one message, not an interview, and they are the only questions the pipeline asks. Everything else it decides. Ask because the cost of guessing lands at the end and cannot be paid there. A venue with a hard 150-word abstract cap is enforced at stage 6 or nowhere: by the time stage 17 writes the abstract, the word budget every section was drafted against is already wrong. A citation style chosen at the gate rather than at stage 6 means the whole draft was written against the wrong marker density. If the user declines to answer, or says "you pick", proceed on the defaults in `references/paper-types.md`, write them into the venue format block at stage 6, and say in one line which defaults you used. Never proceed on an unstated assumption you did not show them. ## Write down the brief's own numbers, then check against them Most briefs arrive with numbers already in them, in ordinary words: "about 3,000 words", "1200-1500 words", "a couple of dozen sources at least", "caps the main text at 6,000 words and the abstract at 200", "thirty or more works", "cover the hospital series, the exposure problem and the policy evaluations". Each of those is a requirement, and each of them is a requirement this pipeline loses if nobody writes it down, because every stage after the first works from an outline rather than from the request. So write them down. At stage 6, alongside the venue format block, record what the user asked for in `research/brief.json`: ```json { "word_range": [2700, 3300], "min_references": 24, "abstract_max_words": 250, "required_sections": ["Introduction", "Methodology", "Discussion", "Conclusion", "References"] } ``` Four rules about that file: - **Every value in it traces to a sentence in the brief**, or to the document type's own skeleton in `references/paper-types.md` where the user left the choice open. A number nobody asked for does not belong in it. - **"About N" is a range, not a point.** "About 3,000 words" is a request to land near three thousand, so record a band around it rather than the single number; ten percent either side is the same tolerance `integrity.py` has always used for `--target`. A stated cap ("no more than 6,000") is a ceiling and is recorded as one. - **Stated minimums are floors, and the only way past one is more sources.** "A couple of dozen at least" records as 24 and stays 24. See "Keep searching until the count is met" below. - **Omit what the brief does not state.** Nothing in the file has a default, and an absent key is simply not checked. An invented cap is as wrong as a missed one. `integrity.py` then reads that file at the gate and checks the draft against it, so the numbers the user stated are tested against the delivered document rather than remembered. ## Four things the scripts own, and you do not A language model is good at judgement and bad at bookkeeping. The split is deliberate, and crossing it is how this output breaks. Each of these is enforced by a script that exits nonzero, so none of them is a matter of opinion at the gate. **Never write a rendered citation marker by hand.** Not `[3]`, not `(Smith, 2020)`. While drafting you write `{cite_}` inline at the exact point of the claim. `scripts/citations.py compile` turns those into markers and builds the bibliography by dictionary lookup. It is deterministic, so the mapping between prose and bibliography is mechanical rather than remembered. **Never invent a source for a claim you cannot support.** If a claim is real and you cannot find a source for it, write `{cite_MISSING: short description of the claim}` and keep going. `compile` refuses to render it, names it, and exits nonzero, and `integrity.py` counts it as critical, so it cannot survive to a finished paper. That is the point: it is not a way to ship an unsourced claim, it is a way to be honest while drafting instead of quietly deleting the claim or attaching it to a DOI you made up. Clear each one before the gate by finding a real source with `sources.py find`, or by cutting the claim on purpose. **Never merge the sections into the draft by hand.** `scripts/assemble.py` does it, in numeric order, and refuses to splice anything that is not a numbered section into the paper. A hand merge that drops one section is the failure nobody notices until a reader does. **Never decide by eye whether the paper is internally consistent.** `scripts/integrity.py` checks it and exits nonzero. Run it and read the exit code. ## Setup ```bash mkdir -p research/texts sections review ``` No API key, no account, no install step: the scripts are Python standard library only. One optional environment variable exists, and it is the only one anything here reads. Set `OPENDRAFT_CONTACT_EMAIL` to your own address and `sources.py` appends it to its User-Agent, which moves Crossref requests into the polite pool and its better rate limits. Leave it unset and every stage still runs; no address is baked in, because a shipped default would pool every installer into one identity and route their rate-limit problems to a stranger's inbox. The pipeline writes to fixed paths, and each stage reads what earlier stages wrote. The paths are the contract between stages: ``` research/sources.md research/sources.json research/summaries.md research/gaps.md research/citations.json research/citation-notes.md research/brief.json research/abstracts.json research/evidence.json research/index.json research/texts/*.txt research/figures/* outline.md outline_formatted.md sections/*.md full_draft.md final.md review/thread.md review/narrator.md review/skeptic.md review/verifier.md review/referee.md review/voice.md review/entropy.md review/polish.md review/evidence.md ``` Four rules about those paths, and each of them has been broken before: - **`sections/` holds paper sections and nothing else.** Every file in it is spliced into the finished paper by `scripts/assemble.py`. A stage report parked there ends up inside somebody's thesis. Reports go in `review/`. - **`research/sources.json` is machine input, `research/sources.md` is human reading.** Stage 1 writes both. `citations.py build` reads the JSON one. - **Nothing hand-written ever goes into `research/citations.json`.** It is built by `citations.py build` and only ever changed by re-running `build`. The judgement calls that no script can make, sources with no DOI, entries needing review, mentions that could not be turned into a placeholder, go in `research/citation-notes.md`, which is prose and is never read by a script. A hand-edited database is how a `verified` flag gets flipped to get past the gate, which is how an unresolved DOI reaches the bibliography looking checked. - **After stage 9.5, `full_draft.md` is the only draft.** Nothing downstream reads `sections/*.md` again, so editing a section file after assembly changes nothing and quietly loses the edit. ## The pipeline Run in order. Every stage but one is a file in `agents/`: read that file, do what it says, write the output it names, then move on. The exception is stage 9.5, which is a script you run. Do not skip a stage because the topic looks easy. | # | Stage | Agent file or script | Writes | | --- | --- | --- | --- | | 1 | Find sources | `agents/01-scout.md` | `research/sources.md`, `research/sources.json` | | 2 | Read and summarise them | `agents/02-scribe.md` | `research/summaries.md` | | 3 | Find the gap worth writing into | `agents/03-signal.md` | `research/gaps.md` | | 4 | Build the citation database | `agents/04-citation-manager.md` | `research/citations.json`, `research/citation-notes.md` | | 4.5 | Extract the evidence (reviews) | `agents/04.5-evidence.md` | `research/abstracts.json`, `research/texts/*.txt`, `research/evidence.json`, `research/index.json`, `research/figures/*`, `review/evidence.md` | | 5 | Outline the argument | `agents/05-architect.md` | `outline.md` | | 6 | Apply venue format and word budgets | `agents/06-formatter.md` | `outline_formatted.md`, `research/brief.json` | | 7 | Write each section | `agents/07-crafter.md` | `sections/*.md`, appends to `research/gaps.md` | | 8 | Check cross-section consistency | `agents/08-thread.md` | fixes in `sections/*.md`, `review/thread.md` | | 9 | Unify voice | `agents/09-narrator.md` | fixes in `sections/*.md`, `review/narrator.md` | | 9.5 | Assemble the sections into one draft | `scripts/assemble.py` | `full_draft.md` | | 10 | Attack the argument | `agents/10-skeptic.md` | `review/skeptic.md`, then fixes | | 11 | Check claims against sources | `agents/11-verifier.md` | `review/verifier.md`, then fixes | | 12 | Simulate peer review | `agents/12-referee.md` | `review/referee.md`, then fixes | | 13 | Match the author's voice (optional) | `agents/13-voice.md` | fixes in `full_draft.md`, `review/voice.md` | | 14 | Vary the prose rhythm | `agents/14-entropy.md` | fixes in `full_draft.md`, `review/entropy.md` | | 15 | Grammar and final polish | `agents/15-polish.md` | `full_draft.md`, `review/polish.md` | | 16 | Add apparatus (optional) | `agents/16-enhancer.md` | `full_draft.md` | | 17 | Write the abstract | `agents/17-abstract.md` | prepended to `full_draft.md` | | 18 | Write the title | `agents/18-titlemaker.md` | prepended to `full_draft.md` | Stage 4.5 runs for literature reviews, scoping reviews and any paper that compares what several studies found about the same outcomes, and is skipped otherwise. It turns findings into `research/evidence.json`, one verbatim quote per finding, and the scripts then compute the evidence index and draw every evidence figure and table from that file. Its number is a half step for the same reason 9.5's is. Stage 7 runs once per section, not once per paper. Stages 10 to 12 produce issue lists; an issue list nobody applies is a no-op, so apply the fixes and re-run the stage until no critical issue remains. ## Keep searching until the count is met Stages 1 and 4 are a loop, not a pass. Run them, then count what survived: ```bash python3 scripts/citations.py verify -d research/citations.json ``` It prints `resolved=N` and that N is the real number: it is what the paper can cite, after the DOIs that turned out to be absent, invalid or unreachable have come out. Compare N against the floor, which is whichever is higher of the `min_references` the brief stated and the per-type floor in `references/paper-types.md`. If N is below the floor, go back to stage 1 and search again. Not the same queries: different ones. The pool ran short because the queries ran out of angles, so widen along the ones the brief itself names, the adjacent literature, the sub-questions the topic decomposes into, the review articles that would cite this work, the outcome measures by name. Then merge into `research/sources.json`, re-run `citations.py build` and `verify`, and count again. Keep going until N clears the floor, or until further queries stop returning anything new. This is where verification discipline turns into its own failure mode. The right instinct, refusing to cite what did not verify, has a wrong ending: a review that drops half its sources at the verification step and then ships the half that survived has not been careful, it has been short. Fifty found and ten verified is not a ten-source review, it is a search that has to continue. The pool is a floor on what you go and find, and the only two honest ways to reach the end of this loop are to meet it or to tell the user plainly, in the delivered document, how many sources you verified and why the literature would not yield more. Never close the gap the other way. Do not lower the floor to what you have, do not cite a DOI whose state is not `resolved`, do not pad the list with sources the paper never cites, and do not count one work twice under two identifiers. Stage 12 has a number, not a feeling. Stop when a fresh run reports zero critical issues and an overall average of at least 3.0 out of 5, with no single dimension below 3 unless you record why in `review/referee.md`. "Good enough to send" is not a stopping condition, because a model asked to judge its own draft will always find it good enough on the third pass. Stage 13 is the only stage with an input the pipeline never produces. It matches the draft to the author's own prose, and it reads that prose from a `samples/` directory in the working directory: two or three prior papers, chapters or long-form posts, as `.md` or `.txt`. Nothing creates that directory and nothing asks you for it, so a run that never makes one skips stage 13 cleanly, which is the normal outcome rather than a failure. Put your own writing there before the run if you want the paper to sound like you wrote it. Stage 9.5 is numbered as a half step because it is a script rather than an agent prompt, and because the eighteen agent stages keep the numbers they already had. It is not optional. Stages 1 to 9 work on `sections/*.md`; stages 10 to 18 work on `full_draft.md`; nothing produces that file except this command: ```bash python3 scripts/assemble.py sections -o full_draft.md ``` It merges the section files in numeric order, refuses to include any file that is not a numbered section, and exits nonzero on a numbering gap, a duplicate number or an empty directory. `--check` reports what it would do without writing. It also skips any numbered file whose name contains `report`, `review`, `notes`, `checklist` or `log`, which is the mechanism that stops a stale stage report ending up inside a thesis. That is a filename heuristic, so it has one sharp edge: a real section called "Review of the literature" would be skipped, and you would see it only as a "Skipped" line on stdout rather than as an error. Name that section `related-work` or `literature` instead. Read the skip lines; a section that vanishes here vanishes silently. Re-running it after `full_draft.md` exists needs `--force`, and `--force` throws away every edit stages 10 and later made to the draft, so re-assemble only when you mean to restart from the sections. ## Scale Match the pipeline to what was asked. The stages are the same; the depth is not. - **A short piece, 1,500 to 3,000 words.** Stages 1 to 7, then 9.5, 10, 11, 15, 17. Ten to fifteen sources, or the brief's own minimum wherever it asks for more. - **A full paper, the default.** All eighteen, plus 9.5, and 4.5 for a review. Twenty-five to fifty sources, or fifty and up when the paper is a literature review, whose own floor governs wherever it is higher (`references/paper-types.md`). - **A thesis chapter or long review.** All eighteen, plus 9.5, and 4.5 for a review, sources in the fifties or more, and stage 7 once per subsection rather than per section. A long review runs 8,000 to 12,000 words on sixty sources or more; its length comes from covering more of the literature and reporting more of it in the evidence tables, never from restating the same findings at greater length. Stage 9.5 is in every one of those lists. There is no scale at which a paper assembles itself. Stage 17 is in every one of those lists too. Every section skeleton in `references/paper-types.md` opens with an abstract, so a tier that skipped the stage that writes one produced a paper missing its first section, and the word it goes under is the only thing scale changes: a journal article has an abstract, a committee paper or an evidence brief has a summary, and both are the same section doing the same job. Write it in the document's own register and keep it inside whatever cap the brief states. Where the brief states none, `agents/17-abstract.md` carries the fallback range. ## The two reference files - **`references/paper-types.md`** carries the section skeleton, word budget and source count for each document type. Read it at stage 5. - **`references/citation-styles.md`** shows the real compiled output of all six styles, in-text marker and reference entry, so you can choose one for the venue and know what it will look like. Read it at stage 6, and again before the gate if a citation renders in a way you did not expect. It also states what the compiler does not carry: no volume, issue or page numbers, no journal abbreviation. If a supervisor requires those, this is the place that says so honestly rather than the place you find out afterwards. Neither file is optional reading dressed up as a reference. A style chosen without reading the second one is a style chosen from memory, and the compiler does not implement your memory of APA. ## The scripts ```bash python3 scripts/sources.py find "" --n 15 # Crossref plus OpenAlex python3 scripts/sources.py find "" --json # machine-readable python3 scripts/sources.py verify ... # Crossref plus DataCite python3 scripts/citations.py build research/sources.json -o research/citations.json python3 scripts/citations.py verify -d research/citations.json python3 scripts/citations.py compile full_draft.md -d research/citations.json --style apa -o final.md python3 scripts/citations.py bibtex -d research/citations.json -o refs.bib python3 scripts/assemble.py sections -o full_draft.md python3 scripts/assemble.py sections -o full_draft.md --check python3 scripts/integrity.py final.md -d research/citations.json --target 8000 python3 scripts/integrity.py final.md -b research/brief.json # the brief's own numbers python3 scripts/integrity.py final.md --stats # count, do not check python3 scripts/integrity.py final.md -c research/summaries.md # advisory number check python3 scripts/export.py final.md --format docx -o final.docx python3 scripts/export.py final.md --format pdf -o final.pdf --template journal --kind "Narrative review" python3 scripts/evidence.py abstracts research/sources.json -o research/abstracts.json python3 scripts/evidence.py check research/evidence.json --abstracts research/abstracts.json --texts research/texts -d research/citations.json python3 scripts/evidence.py index research/evidence.json -o research/index.json python3 scripts/evidence.py figures research -o research/figures ``` Every one of them exits nonzero on failure. That exit code is the signal; read it rather than skimming the output. `sources.py find` exits 1 when no API could be reached at all, which is different from reaching them and getting no hits. `agents/01-scout.md` is the authority on how to read that exit code and on the source floor below which stage 1 stops rather than proceeding; read it there rather than trusting an empty result. `citations.py build` takes the JSON array stage 1 wrote to `research/sources.json`, not the markdown in `research/sources.md`. Feeding it the markdown file is an error, and it will say so. Search two or three narrower sub-queries as well if the first pass is thin. A paper with five sources reads like one with five sources. ## Length is a number, not an impression Count the draft before delivering it, and count it again after every trim: ```bash python3 scripts/integrity.py full_draft.md --stats ``` That prints the main text's word count, the abstract's, the number of reference entries and the headings. Main text means the body: the reference list and the abstract are counted separately, because a brief that caps the main text and the abstract as two numbers is treating them as two numbers. Do this at stage 15, before the gate, and act on what it says. Over the range, cut, and cut the paragraphs that repeat an argument already made rather than shaving a word from every sentence. Under it, go back to the sources and write what the pool actually supports; if the pool does not support more, that is a finding about the literature, and it goes in the paper as one instead of being padded over. The estimate is the thing to distrust here. A draft that feels like five thousand words can be nine, and a paper that comes in half again over a stated cap is not a long paper, it is one the venue will not take and the reader will not finish. Neither the model that wrote the prose nor the person reading it back can tell six thousand words from nine and a half by eye; the command above can, and it takes a second. ## The docx a reader opens `scripts/export.py` sets the body font, the body size and the page margins on the exported docx, through the document's Normal style rather than stamped onto each paragraph, so a reader who restyles Normal restyles the paper. The defaults are Times New Roman, twelve point, one inch margins, which is the ordinary manuscript setting; `--font`, `--font-size` and `--margin-inches` change them where a venue's house style asks for something else. This is set rather than left alone because pandoc's own default names no body font at all, which means the file opens in whatever the reader's word processor calls Normal, and on a current Word that is Calibri. A literature review for a supervisor, a brief for a committee and a manuscript for a journal are all documents whose typography somebody is expected to have decided. Arriving in the word processor's default is the one outcome that says nobody did. ## The journal PDF `scripts/export.py --template journal` typesets `final.md` as a two-column journal article through `scripts/journal.py`: a masthead, a serif title with the subtitle split off at the first colon, a two-column abstract, numbered sections, merged runs of adjacent author-year citations, and, when stage 4.5 ran, an at-a-glance strip, the evidence figures, the two evidence tables and the index equations at the draft's placeholder lines. The fonts, STIX Two Text and Source Sans 3, ship in `assets/fonts/` under the SIL Open Font License. It needs pandoc and, for the PDF, weasyprint. It invents nothing a journal page usually carries. There is no journal name, volume, issue, received date, affiliation or DOI unless you pass it: `--brand` sets the masthead name (default OpenDraft), `--kind` the article type line, `--byline` and `--masthead-note` the two lines of small print, and each takes only what is true of this paper. A placeholder whose figure was never drawn stops the export with the command that draws it, rather than printing the braces or leaving a hole. The docx, latex and plain html exports cannot place those figures, so they drop the placeholder lines and name each one on stderr. ## Verification, and what it does and does not prove `citations.py verify` puts every DOI into one of four states, and `unknown` is never quietly turned into `absent`: - **resolved.** Crossref or DataCite returned the record. Cite it. - **absent.** Both returned 404. Drop it. Do not repair it, do not guess a replacement DOI, go back to stage 1 for a real source. - **unknown.** A network or rate-limit failure. Retry once, then name it in the paper's limitations rather than pretending it resolved. - **invalid.** Malformed or empty. arXiv preprints resolve at DataCite and 404 at Crossref, which is why the check runs both. A Crossref-only check silently deletes every preprint. `citations.py compile` refuses to render any citation whose state is not `resolved`, and names every offender. That is what makes the DOI claim in this skill's own description true rather than aspirational: a DOI that never resolved cannot reach the printed bibliography, because the compiler will not print it. The fix for a refusal is a real source, never a softer claim. This makes `verify` mandatory rather than advisory, and the order is not cosmetic. `build` writes `verified: "unknown"` for every record it creates, so a `compile` run before a `verify` run refuses every citation in the paper and exits 1. If that happens, you have not found a bug; you have skipped a step. A source with no listed authors is citable. It renders as `Anon.` in the marker and at the head of its bibliography entry, in all six styles. Do not discard such a source, and never supply an author name it does not have. A resolved DOI proves the work exists. It does not prove the work supports the sentence citing it. That second question is stage 11, and it is the one that matters most. ## The gate, non-negotiable Before showing anyone the paper: ```bash python3 scripts/citations.py verify -d research/citations.json python3 scripts/citations.py compile full_draft.md -d research/citations.json --style