Source triage
Finds candidate sources, then answers four questions about each one before any analysis:
- Is it legally free? (access)
- Can I, an AI agent, actually read it, and by which route? (reach)
- Is it real, current and unretracted? (integrity)
- How good and how central is it? (grade, role, ⭐ context pick)
The output is a ledger. research-plan builds on it, evidence-check audits against it, and the user can use it on its own ("which of these can you read, which must I download?"). Works for any field and for non-paper sources.
Core truths (learned the hard way; do not relearn them)
- Access is per route, not per paper. An OA article can be blocked on the publisher site and readable through an API, and the reverse. Record the route that worked.
- HTTP 200 lies. CAPTCHA pages, login walls, JS shells, empty-body XML, and tools that report "success" with empty text all look like success. Check what you got.
- WebFetch returns a model's extraction, not the page. Good for reading and for metadata; never a source of verbatim quotes. Verbatim text comes only from raw routes: API XML/JSON fetched with the shell (
curl), a downloaded PDF, or a file the user gives. - Blocked ≠ missing. A page that blocks you leaves a source
unchecked, nevernot_found. An API that is down means omit the signal, never recordfalse. - Bronze ≠ readable. Test it.
- Never circumvent. No shadow libraries, mirrors, caches, scraping round robots/CAPTCHA, or bulk harvesting.
- Never name a source from memory. In trials, both models' first-draft references had wrong titles, wrong years, a wrong DOI and one paper that does not exist. Every source is looked up before it appears anywhere.
Write as you go (checkpoint)
Create research/<slug>/ledger.md at the start. After every search, append the query line to the search log. After classifying a source, append its row straight away. Long runs lose context; the ledger on disk is the memory.
Step 0 — Frame (one message, then go)
State in one line: question · field(s) · year range (default: last 10–15 yrs + landmark older work; "recent" = 5 yrs) · source types in scope · hard constraints from the request (e.g. "rubric needs 2 book chapters") · depth:
quick: keep 8–15 sources. Snowballing is optional (one forward snowball from the top review). Records beyond the cap are logged asnot carried forward (cap): counted, but no exclusion reason needed.deep: keep 25+, snowball backward and forward from the 3–5 most central works.
Proceed unless corrected. If the user said "check with me" / "step by step", stop at each ◆. Otherwise run straight through and list your choices at the end.
Step 1 — Library first
Check what the user already holds: a connected folder of PDFs, earlier research/*/ ledgers, a .bib or Zotero/CSL export. Owned sources are held: library. Pass their DOIs to the search as "already have".
Step 2 — Discover
Term set per concept, ORed together: synonyms, acronyms, older names, UK/US spellings, method names. For life science add MeSH + entry terms, gene symbols, enzyme names + EC numbers, Latin + common organism names. Query the method, not the author. Use 2–3 concept groups per query; more ANDs return 0 hits.
| Need | Primary | Also |
|---|---|---|
| Biomedical / life science | PubMed (MeSH) | Europe PMC REST (SRC:"PPR" finds bioRxiv/medRxiv preprints; HAS_FT:y = full text available), OpenAlex |
| Any field, broad | OpenAlex (topic/concept filters beat raw search) | Semantic Scholar, Crossref |
| CS / ML / physics / quant-bio | arXiv (sort by relevance) | Semantic Scholar, alphaXiv / Scholar Feed |
| Preprints | Europe PMC SRC:"PPR" or OpenAlex type:preprint | the bioRxiv connector has no keyword search: use it only for a known DOI's details and published version |
| Book chapters | OpenAlex type:book-chapter, publisher catalogue, WebSearch | verify the container (see Step 4) |
| Claims / reviews fast | Consensus, Elicit (if connected) | verify every hit by DOI |
| Non-paper | the owner's official docs/spec/code first | WebSearch for the rest |
- Sensitivity check: if key papers are known, confirm your queries find them.
- Hit counts: tens of thousands of hits means too broad; 0–1 means over-constrained, so reword. Rewording a zero-hit query is part of the same round.
- Record: title, authors, year, venue, type, DOI · PMID · PMCID · arXiv · URL,
found_via. - Dedupe: DOI → other IDs → normalized title + year. Merge preprint + published version.
- Bounds: ask before ~1,000 records or ~50 calls.
◆ Checkpoint: terms, queries, hit counts.
Step 3 — Access triage (before reading anything)
3a. Legal access (batch it)
For each kept source, call OpenAlex get_work (search results omit licence). Copy its oa_status value exactly: gold · diamond · hybrid · green · bronze · closed. Also record license and oa_url/pdf_url. Europe PMC license fields and PubMed get_copyright_status are fallbacks. Unpaywall (version, host_type) needs a real email: use only one the user approved for this; otherwise skip it.
- No data →
unknown. Never inventoa,?suffixes or publisher names as access values. - Non-paper source →
web. - Licence unknown → treat as
local-onlyfor redistribution.
3b. Agent reach: the route ladder (stop at the first success)
- Held: in the user's library or an earlier run.
- Europe PMC REST, raw, via shell
curlwhere the shell can reach it:- search:
…/rest/search?query=DOI:"a" OR DOI:"b"&format=json, ≤8 DOIs per call, ≥1 s apart; - full text:
…/rest/{PMCID}/fullTextXML. This is the best verbatim route. Use WebFetch only if the shell can't reach it; WebFetch rejects long URLs and is rate-limited. - 429 → wait, retry once. 5xx →
blocked:http-5xx, go to the next route.
- search:
- PubMed
get_full_text_article: valid only iffull_textis non-empty. Empty →blocked:empty-body. - Preprint servers: arXiv, bioRxiv/medRxiv
.fullpages. - OA locations: OpenAlex
oa_url/pdf_url(a direct PDF URL may work where the article page fails), Semantic ScholaropenAccessPdf, CORE, Crossref TDM links. - Publisher page: one attempt per host. Following the DOI redirect is that attempt. Never follow into a robots-disallowed host (ScienceDirect / Elsevier linkinghub).
- Author or institutional repository via WebSearch
"<exact title>" pdf. Respect "no text-and-data-mining / AI use" notices. - Nothing worked: record the reason and route the source to the user.
Observed 2026-10 (re-test; these change):
- Raw OK: Europe PMC REST via curl.
- Extracted OK: Nature OA, Frontiers, MDPI, bioRxiv
.full, arXiv, Springer PDF URLs from OpenAlex. - Blocked: ScienceDirect (robots), Wiley incl. OA (fetch error), Springer article pages, BMC PDF endpoint, PMC website (CAPTCHA), Annual Reviews, Science.
- Unreliable: PubMed full-text tool (often empty), Europe PMC fullTextXML (HTTP 500 for some PMCIDs), Europe PMC via WebFetch (rate-limited).
Validate before calling anything readable:
- body text beyond the abstract;
- not a CAPTCHA / sign-in / access-denied page;
- not empty or metadata-only XML.
- A downloaded file counts only if it starts with
%PDF-, is not HTML, and is over ~10 KB.
| reach | meaning |
|---|---|
readable:<route> | full text obtained: readable:europepmc-xml (raw) · readable:pdf (raw) · readable:webfetch (extracted, not verbatim-capable) |
abstract-only | only abstract obtainable (typical paywall) |
blocked:<reason> | paywall (not even an abstract) · robots · captcha · login · http-4xx · http-5xx · fetch-error · rate-limited · empty-body · js-shell |
unchecked | not attempted (budget / cap). Never write "not tested". |
Label rule: the controlled word comes first, exactly as in these tables. Notes may follow in parentheses, e.g. verified (Crossref DOI).
Look for a green copy before you label a source blocked or abstract-only. A source that is legally gold/hybrid/green/bronze but blocked for you goes in the "free but blocked for AI" section.
Step 4 — Integrity (batched; before naming anything)
Run this right after discovery for every source you will name: in the plan, as ⭐, or as a recommendation. It is not a final step.
- Existence: verdicts
verified·partial·mismatch·not_found·unchecked. Use Reference Checkercheck_references+ OpenAlexresolve_references. When tools disagree, an exact DOI match in Crossref or OpenAlex beats a title-searchnot_found; record both. - Book chapters: verify the container, not just the DOI: book title, editors, pages and publisher from the Crossref record (
api.crossref.org/works/<doi>→container-title,page). In trials a DOI was right but the book title was wrong. - Retraction:
check_retractions. Clear means only "no notice found". - Peer review: label preprints (
peer: preprintin the ledger;`preprint`on the sources-file entry); cite the published version if one exists. - Contamination: a 2024+ preprint that is unmatched in ≥2 indexes is a fabrication risk. Re-verify it or drop it.
- Predatory venue flags; grey literature: URL + accessed date + Wayback link.
Step 5 — Grade, role, ⭐
Grade A–F: median of fitness for its own field, methodology, peer review, currency, data, conflicts. An integrity failure caps the grade at D. An unread or abstract-only source gets a provisional grade (B?).
Role: evidence · background · method · tool/data · counter.
Non-paper tiers: T1 owner docs/spec/code/statute · T2 institutional · T3 expert journalism · T4 community · T5 vendor marketing.
⭐ Context pack (5–10 picks): the sources that would most improve what is being built, if stored locally. Access is irrelevant. Each pick needs:
- why it matters;
- how to get it (OA link / VIT library login / interlibrary loan / email the author);
- a Save-as name
<firstauthor>-<year>-<keyword>.pdf; - redistribute:
ok(CC-BY / CC0 / public domain) orlocal-only(everything else, including unknown licence).
Skew check: ≥70% of known values in one dimension → say so with the count.
Step 6 — The ledger
research/<slug>/ledger.md: the first table, one row per source, columns in this order:
# · Short cite · Type · Role · Grade · access · licence · version · reach · held · read · integrity · retraction · peer · ⭐ · Save as · Link · found_via
readstarts asnot-read;research-planfills infull·sections:<which>·abstract·metadata.⭐=okorlocal-onlyfor picks,-otherwise.- Excluded rows use ids
E1….
Below the table:
- Search log: database · date · query · hits · which term found what.
- Counts: identified → after dedupe → not carried forward (cap) → kept → excluded (with reasons).
totalin any Counts line = every ledger row, including excluded.
Run the bundled checker, python3 <this skill's folder>/scripts/research_checks.py ledger research/<slug>/ledger.md (standard-library Python, runs in your own sandbox), and fix errors before handing on. Used on its own, render the ledger as the sources file described in research-plan.
Privacy
Search with topic words only, never sentences from the user's unpublished work. The user's email goes only to Unpaywall, and only with their go-ahead.