ref-manager

Commands · Write & compare

Turn a set of papers into tables, prose, and citations

These commands take papers you already hold and produce something you can write from: an evidence matrix, a methods roll-up, a narrative summary, an appraised review, a checked paragraph, a saved answer, or a bibliography. Most of them take the same selector and freeze what it resolved to, so a saved artifact never changes membership behind your back.

How the synthesis commands differ

All set-valued commands here share one selector grammar (<pmid…>, --project, --study, --query, --search, --from-file, refined by --tier and --exclude; see Concepts). The selector is resolved once, reported with counts by extraction tier and retraction status, and written into the artifact's manifest.json. Re-running with the same --batch (or --key) reuses that frozen set; only --refresh re-resolves, and then it reports what was added or removed.

selector <pmid…> · --project --study · --query · --search --tier · --exclude RESOLVE & REPORT lib_selector.resolve() counts by tier · retraction status empty match is an error FREEZE → manifest.json selector_expression · pmids · report same --batch / --key reuses this set --refresh re-resolves, reports the diff compare structured matrix 9 columns per paper cells → claim_ids rows grouped by study table.json summarize prose narrative all active claims → ref-synthesizer [^pmid] citations summary.md review appraised synthesis RoB 2 · NOS · AMSTAR-2 set-level GRADE drafts until reviewed grade.json review --prisma search flow counts runs · screening · full_text · studies unbacked → "unknown" flow.md · flow.csv brief saved answer snapshot per save refresh only on ask diff: new · withdrawn answer.md check-citations assertion verdicts supported · overstated conflicting · insuff. unavailable findings.json NO LLM CALL IN THE SCRIPT compare · review · review --prisma are pure queries over committed claims, screening and study records. JUDGMENT HAPPENS UPSTREAM summarize · brief use ref-synthesizer; check-citations is judged inline by the Claude session. The script only validates and persists.
frozen resolution uses the ref-synthesizer subagent takes --project + saved queries, not a selector
/ref:brief and /ref:check-citations run retrieval like /ref:ask, so a selector only scopes their search; they freeze the evidence they used rather than a batch membership. /ref:methods (not drawn) resolves a selector but saves nothing.

The export path

meta.json is the bibliographic authority. There is exactly one path from it to a citation format, so BibTeX and CSL-JSON can't drift apart. The Papers handoff reuses the same CSL-JSON fields but renders Papers' own BibTeX dialect.

meta.json + authorship.json citekey · title · journal CSL-JSON lib_cite.to_csl() one generation path /ref:export → exports/<batch>/ references.csl.json → references.bib (rendered from CSL) manifest.json · frozen, reused unless --refresh /ref:export-papers → <dest>/ references.bib in Papers' dialect (+abstract, pages, local-url) PDFs copied or linked from raw/<sha256>/source.pdf manifest.json · .ref-manager-written.json ONE-WAY You import references.bib through Papers' UI; its database is only ever read, for duplicates.
/ref:cite skips this path entirely: it just reads the citekey from meta.json. /ref:check-citations --export-bib reuses the same build_exports function for the PMIDs its report cites.

Examples below that say output from a scratch library were run against a throwaway library holding two synthetic, abstract-only papers (PMIDs 10000001 and 10000002, in project thesis-ch3) with HOME pointed at a scratch directory. Long JSON is trimmed where marked.

Commands

/ref:compare

phase 5

Build a frozen evidence matrix over a selected set of papers, where every cell points to the claims behind it or says why it is empty.

/ref:compare <selector> --batch <label> [--refresh] /ref:compare --batch <label> [--project <slug>] --edit <pmid> <column> <value>

Arguments

<selector>
Any §5c selector: <pmid…>, --project [--question], --screened, --read/--queue, --query [--run], --study, --search, --from-file, refined by --tier/--exclude. --concept is rejected with "not available until phase 8".
--batch <label>
Required. Names the table. Re-running without --refresh returns the frozen table unchanged.
--refresh
Re-resolve the selector, rebuild cells, and report added, removed, changed_evidence_cells, and retraction_status_changes.
--edit <pmid> <column> <value>
Record your own override for one cell (the script takes it as --edit-pmid/--edit-column/--edit-value). Pass --project if the table was built with one.

What happens

  1. Resolves the library root; fails pointing at /ref:init if unconfigured.
  2. If the batch exists and --refresh is absent, returns reused_frozen_table without touching the selector.
  3. Otherwise resolves the selector and builds rows. Papers sharing a recorded study (/ref:study) share one row, with the study's confidence shown; shared datasets alone never group papers.
  4. Fills nine columns per paper: population, design, methods, sample_size, comparator, results, uncertainty, limitations, relevance. not_extracted means no claim covers that dimension (sample_size and limitations always, since extraction doesn't capture them); not_reported means claims exist but the paper didn't state it. The two are kept distinct on purpose.
  5. Each row also carries a provenance block keyed by PMID with extraction tier, last checked timestamp, and whether the record currently has an abstract or full text available.
  6. On refresh, re-applies your edits: an edit whose underlying cell is unchanged survives; one whose evidence changed is flagged stale, never silently dropped or silently re-applied.
  7. Writes the table, then the manifest (including a per-PMID retraction-status snapshot for the next refresh).

Reads & writes

Reads papers/<pmid>/meta.json, claim registries, studies/ records, projects/<slug>/papers.yaml (relevance). Writes projects/<slug>/tables/<batch>/{manifest.json, table.json, edits.json}, or tables/<batch>/ at the library root without --project. A bare PMID list and an equivalent --project produce identical rows; only selector_expression differs.

Example

output from a scratch library

 /ref:compare --project thesis-ch3 --batch ch3-matrix
{
  "status": "created",
  "batch": "ch3-matrix",
  "manifest": {
    "batch": "ch3-matrix",
    "selector_expression": "--project thesis-ch3",
    "pmids": ["10000001", "10000002"],
    "project": "thesis-ch3",
    "report": {
      "count": 2,
      "by_extraction_tier": {"abstract": 2, "full": 0, "unavailable": 0, "missing_record": 0},
      "by_human_verification_state": {"not_yet_tracked": 2},
      "by_retraction_errata_status": {"unknown": 2}
    },
    "retraction_status_snapshot": {"10000001": "unknown", "10000002": "unknown"}
  },
  "rows": [
    {
      "pmids": ["10000001"],
      "study": null,
      "cells": {
        "10000001": {
          "population": {"value": "not_extracted"},
          "design": {"value": "not_extracted"},
          # … remaining columns and the second row trimmed; these papers have no claims yet
        }
      }
    }
  ]
}

Under the hood

compare.py --repo <root> [selector] --batch <label> [--refresh], or with --edit-pmid/--edit-column/--edit-value. No subagent.

See also

/ref:methods · /ref:review · /ref:study · /ref:extract

/ref:methods

phase 5read-only

List what is known about each selected paper's design, adjustment, cohort, and linked methods and datasets, without guessing the rest.

/ref:methods <selector>

Arguments

<selector>
Same grammar as /ref:compare. No --batch: nothing is saved.

What happens

  1. Resolves the selector and prints its report.
  2. Groups papers by recorded study, as /ref:compare does.
  3. Per paper, collects distinct study_design, adjustment_context, cohort_identity from active claims, plus methods and datasets explicitly linked with /ref:study create-method/create-dataset.
  4. Each per-paper row also includes provenance metadata (extraction_tier and checked_at) so you can see freshness and source state without opening raw files.
  5. instruments, software, and controls always read not_reported: the claim schema doesn't capture them, and the command won't reconstruct protocol details from what similar papers usually do.

Reads & writes

Reads claim registries and studies/ method, dataset, and study records. Writes nothing.

Example

output from a scratch library

 /ref:methods 10000001
{
  "report": {
    "count": 1,
    "by_extraction_tier": {"abstract": 1, "full": 0, "unavailable": 0, "missing_record": 0},
    "by_human_verification_state": {"not_yet_tracked": 1},
    "by_retraction_errata_status": {"unknown": 1}
  },
  "rows": [
    {
      "pmids": ["10000001"],
      "study": null,
      "per_paper": [
        {
          "pmid": "10000001",
          "study_design": "not_reported",
          "adjustment_context": "not_reported",
          "cohort_identity": "not_reported",
          "linked_methods": "not_reported",
          "linked_datasets": "not_reported",
          "instruments": "not_reported",
          "software": "not_reported",
          "controls": "not_reported"
        }
      ]
    }
  ]
}

Under the hood

methods.py --repo <root> [selector]. No subagent.

See also

/ref:compare · /ref:study

/ref:summarize

phase 10agent

Write a cited prose narrative over a selected set of papers (or one paper), saved as a frozen batch.

/ref:summarize <selector> --batch <label> [--refresh]

Arguments

<selector>
Required. Any §5c selector; it fixes the paper set.
--batch <label>
Required. Names the saved summary; reused unless --refresh.
--refresh
Re-resolve and regenerate, reporting added_pmids, removed_pmids, withdrawn_evidence, and retraction_status_changes.

What happens

  1. Runs the script with --dump-candidates to get the resolution report and the evidence before anything is written.
  2. The candidates are every active claim of the resolved papers, not ranked search hits: the selector already chose the set.
  3. An abstract-only set may still be summarized, but the summary must say so. (Contrast /ref:review, which refuses.)
  4. Spawns one ref-synthesizer for the whole set, never one per paper, with a fixed instruction to cover populations, interventions, outcomes, direction of effect, disagreement, tier, and retraction status.
  5. The saved manifest includes a provenance block with the resolved PMIDs and the project/question context so a brief can be traced to the exact library slice that generated it.
  6. Saves the returned answer with its coverage note and unresolved questions. One paper and many papers go through the same code path.

Reads & writes

Reads claim registries and meta.json. Writes projects/<slug>/summaries/<batch>/{manifest.json, summary.md, evidence.json}, or summaries/<batch>/ without --project.

Example

output from a scratch library (step 1 only)

 /ref:summarize 10000001 --batch rivera-summary
# step 1: resolve and collect candidates, before the subagent runs
{
  "resolution": {
    "pmids": ["10000001"],
    "selector_expression": "--pmid 10000001",
    "report": {
      "count": 1,
      "by_extraction_tier": {"abstract": 1, "full": 0, "unavailable": 0, "missing_record": 0},
      "by_human_verification_state": {"not_yet_tracked": 1},
      "by_retraction_errata_status": {"unknown": 1}
    }
  },
  "candidates": []
}
# empty: this synthetic paper has no extracted claims, so there is nothing to summarize

illustrative output

# after ref-synthesizer returns, the saved batch is reported as
{
  "status": "created",
  "batch": "ch3-summary",
  "manifest": {
    "batch": "ch3-summary",
    "project": "thesis-ch3",
    "selector_expression": "--project thesis-ch3",
    "pmids": ["…", "…"],
    "report": { … },
    "coverage_note": "…",
    "unresolved_questions": ["…"]
  },
  "summary": "… prose with [^pmid] citations …"
}

Under the hood

summarize.py --repo <root> [selector] --dump-candidates, then summarize.py … --batch <label> --answer-file <tmp> [--coverage-note …] [--unresolved …] [--refresh]. The script also accepts --show with --batch to print a saved summary. Subagent: ref-synthesizer.

See also

/ref:ask · /ref:brief · /ref:compare

/ref:review

phase 10--prisma: phase 5

Either appraise a selected set (risk of bias per paper, GRADE certainty for the set) or, with --prisma, count a project's search flow from committed records.

/ref:review <selector> --batch <label> [--refresh] /ref:review --prisma --project <slug> [--query <slug>[:<run_id>] …] [--refresh]

The two modes share a name but nothing else. If the arguments are neither --prisma nor a selector, the command stops rather than guessing.

Arguments · appraised mode

<selector>
Required. Any §5c selector.
--batch <label>
Required. Names the saved review; reused unless --refresh.
--refresh
Re-resolve and regenerate; picks up review decisions recorded since, and reports added_pmids/removed_pmids.

What happens · appraised mode

  1. Checks tiers first. If the resolved set has no full-tier paper, returns "status": "refused" and generates nothing: RoB 2, Newcastle-Ottawa, AMSTAR-2 and GRADE need full-text detail.
  2. Picks a checklist per paper from the claims' study type: RCT → RoB 2, cohort/case-control → Newcastle-Ottawa, meta-analysis → AMSTAR-2 (a representative subset of items). Other study types get a note, not a forced rating. Abstract-tier papers in a mixed set are marked insufficient_information, not dropped.
  3. Rates each domain deterministically from claim fields, with claim_ids and a note. No LLM is involved. Most domains land on insufficient_information because the claim schema doesn't record things like blinding or allocation concealment; that is the honest answer, not a bug.
  4. Computes set-level GRADE: a baseline from study types, then each downgrade factor listed with downgrade, not_assessed, and reason. Nothing is silently omitted.
  5. Every rating starts as model_draft. Record a human decision per domain with verify.py review-appraisal (/ref:verify); a refreshed review then shows human_confirmed, human_edited or human_rejected. GRADE itself has no per-paper home and lives in grade.json.

Reads & writes · appraised mode

Reads claim registries, meta.json, corrections.json review decisions, relation records. Writes projects/<slug>/reviews/<batch>/{manifest.json, grade.json, appraisals/<pmid>.json}, or reviews/<batch>/ without --project. A refusal writes nothing.

Arguments · --prisma

--project <slug>
Required.
--query <slug>[:<run_id>]
Repeatable. A saved query whose run history feeds "identified"; without :run_id its latest run is used. Projects don't record their queries, so you name them each time. Omit it and identified/duplicates read "unknown", not zero.
--refresh
Re-derive from current state and report included_added/included_removed; otherwise the last snapshot is returned verbatim.

What happens · --prisma

  1. Identified and duplicates come from queries/<slug>.yaml run histories; screened and excluded (by reason) from projects/<slug>/screening.jsonl; sought and not-retrieved from each included paper's full_text flag; included studies from studies/studies.jsonl.
  2. Nothing is re-run or estimated. Counts with no committed evidence read "unknown" and are named in unresolved_query_specs or unevidenced_* rather than fudged to make the arithmetic balance.
  3. Included publications and included studies are reported separately, since several papers can report one study.

Reads & writes · --prisma

Writes projects/<slug>/prisma/<snapshot_id>/{manifest.json, flow.md, flow.csv} and projects/<slug>/prisma/latest.json. Snapshot IDs are opaque (prisma-…).

Example

output from a scratch library

 /ref:review --project thesis-ch3 --batch ch3-review
{
  "status": "refused",
  "batch": "ch3-review",
  "reason": "selected set is wholly abstract-tier -- appraisal needs full-text detail; not proceeding (§8 phase-10 gate)",
  "by_extraction_tier": {"abstract": 2, "full": 0, "unavailable": 0, "missing_record": 0}
}
# fetch or attach full text and run /ref:extract first

 /ref:review --prisma --project thesis-ch3
{
  "status": "created",
  "snapshot_id": "prisma-f64fae44bffb",
  "manifest": {
    "snapshot_id": "prisma-f64fae44bffb",
    "project": "thesis-ch3",
    "query_specs": [],
    "flow": {
      "identified": {
        "per_source": {},
        "total_raw": "unknown",
        "note": "no --query given: identified counts are unknown, not zero"
      },
      "duplicates_removed": "unknown",
      "unresolved_query_specs": [],
      "screened": 0,
      "excluded": {"count": 0, "by_reason": {}},
      "unevidenced_screened_pmids": [],
      "reports": {"sought": 0, "not_retrieved": 0, "not_retrieved_pmids": [], "unevidenced_sought_pmids": []},
      "included": {
        "publications_count": 0,
        "publications": [],
        "studies_count": "unknown",
        "studies_note": "studies/studies.jsonl not available -- included-studies count is unknown, not assumed equal to included-publications count",
        "grouped_studies": [],
        "ungrouped_publications": []
      }
      # … project and data_cutoff fields trimmed
    }
  }
}

Under the hood

Appraised: appraise.py --repo <root> [selector] --batch <label> [--refresh]; reviews recorded with verify.py review-appraisal --pmid … --checklist RoB2|Newcastle-Ottawa|AMSTAR-2 --domain-key … --decision accept|edit|reject --reviewer … --rationale …. PRISMA: prisma.py --repo <root> --project <slug> [--query …] [--refresh]. No subagent in either mode.

See also

/ref:screen · /ref:search-pubmed · /ref:verify · /ref:study · Systematic review tutorial

/ref:check-citations

phase 7

Split a paragraph into assertions and record, for each, whether library evidence supports it, without ever rewriting your text.

/ref:check-citations "<paragraph>" [<selector>] [--export-bib] /ref:check-citations --file <path> [<selector>] [--export-bib]

Arguments

<paragraph> | --file <path>
Required. The text to check.
<selector>
Optional: --project, --study, --search, --from-file, or a bare PMID list, to scope retrieval.
--export-bib
Also write BibTeX and CSL-JSON for every PMID the report cites as evidence.

What happens

  1. Notes any [^pmid] markers already in the paragraph: those citations must be verified, not just searched around.
  2. Retrieves candidates like /ref:ask (query expansion, then ask_retrieve.py), and adds the active claims of any cited PMID that retrieval missed.
  3. The Claude session itself splits the paragraph and gives each assertion one verdict: supported, overstated, conflicting, insufficient, or unavailable. An existing citation that doesn't back its sentence gets citation_mismatch: true. No subagent is used, since this verifies text rather than writing it.
  4. The script validates the findings: each assertion_text must be an exact substring of your paragraph, every evidence PMID and claim ID must be in the candidate set, and unavailable must carry no evidence.
  5. Persists the paragraph verbatim, the findings, and verdict counts, and always prints the coverage caveat.

Reads & writes

Writes checks/<check_id>/{input.md, findings.json, manifest.json} (or under projects/<slug>/checks/), plus references.bib and references.csl.json with --export-bib. Check IDs are opaque (check-…); each check is a one-off snapshot.

Example

illustrative output

 /ref:check-citations "Cortical thickness is reduced in autistic adults [^10000001]. This causes social difficulties." --project thesis-ch3
{
  "check_id": "check-…",
  "manifest": {
    "check_id": "check-…",
    "project": "thesis-ch3",
    "selector_expression": "--project thesis-ch3",
    "referenced_pmids": ["10000001"],
    "verdict_counts": {"supported": 1, "overstated": 0, "conflicting": 0, "insufficient": 0, "unavailable": 1},
    "caveat": "Available library coverage does not establish a comprehensive literature check (§5a) -- an 'unavailable' or 'insufficient' verdict means this library doesn't have evidence either way, not that none exists.",
    "bibliography_exported": false
  },
  "findings": [
    {"assertion_text": "Cortical thickness is reduced in autistic adults", "verdict": "supported",
     "evidence": [{"pmid": "10000001", "claim_id": "…"}]},
    {"assertion_text": "This causes social difficulties.", "verdict": "unavailable", "evidence": []}
  ],
  "caveat": "…"
}

Under the hood

ask_retrieve.py for candidates, then check_citations.py check --repo <root> [--project <slug>] --paragraph-file … --findings-file … --candidates-file … [--resolution-file …] [--export-bib]. check_citations.py show --id <check_id> reprints a saved check. Judgment is inline in the session; no subagent.

See also

/ref:ask · /ref:export · /ref:audit · Thesis chapter tutorial

/ref:brief

phase 6agent

Save a grounded answer to a recurring question with its evidence, and change it only when you ask for a refresh.

/ref:brief "<question>" --key <label> [--project <slug>] [<selector>] [--refresh] /ref:brief --key <label> [--project <slug>] --show /ref:brief --key <label> [--project <slug>] --edit "<text>"

Arguments

<question>
Required on first save.
--key <label>
Required. Your name for this standing question. Distinct from the opaque snapshot_id minted on every save.
--project <slug>
Optional; omit for a library-wide brief.
<selector>
Optional, constrains retrieval as in /ref:ask.
--refresh
Re-run retrieval and synthesis, save a new snapshot, report the diff.
--show
Print the current snapshot without running anything.
--edit <text>
Attach your revision to the current snapshot.

What happens

  1. --show and --edit call the script directly and stop.
  2. Otherwise runs the same sequence as /ref:ask: query expansion, ask_retrieve.py, one ref-synthesizer, then validate_citations.py.
  3. If a snapshot exists and --refresh is absent, returns reused_frozen_brief. There is no scheduled monitoring: a brief never updates on its own.
  4. On refresh, reports added_support_claim_ids, new_pmids, withdrawn_evidence (claims no longer active or rejected via /ref:verify), and retraction_status_changes. Deciding whether new evidence conflicts is left to the synthesis step; the script only reports what it can check deterministically.
  5. Your --edit survives a refresh whose evidence is unchanged, and is marked "stale": true (kept, not discarded) when it changed.

Reads & writes

Writes projects/<slug>/briefs/<key>/{latest.json, edits.json, <snapshot_id>/{manifest.json, answer.md, evidence.json}}, or briefs/<key>/ at the library root. Old snapshots stay on disk.

Example

illustrative output

 /ref:brief --key thickness --project thesis-ch3 --refresh
{
  "status": "refreshed",
  "snapshot_id": "brief-…",
  "manifest": {
    "snapshot_id": "brief-…",
    "key": "thickness",
    "project": "thesis-ch3",
    "question": "Is cortical thickness altered in autistic adults?",
    "selector_expression": "<all>",
    "pmids_at_resolution": null,
    "unresolved_questions": ["…"],
    "evidence_hash": "…"
  },
  "answer": "… [^pmid] …",
  "added_support_claim_ids": ["…"],
  "new_pmids": ["…"],
  "withdrawn_evidence": [
    {"claim_id": "…", "pmid": "…", "reason": "excluded_from_synthesis (rejected via /ref:verify)"}
  ],
  "retraction_status_changes": [],
  "user_edit": {"revision": "…", "based_on_evidence_hash": "…", "edited_at": "…", "stale": true}
}

Under the hood

brief.py save --repo <root> --key … [--project …] --question … --answer-file … --evidence-file … [--resolution-file …] [--unresolved …] [--refresh]; brief.py show; brief.py edit … --revision-file …. Subagent: ref-synthesizer.

See also

/ref:ask · /ref:summarize · /ref:verify · /ref:audit

/ref:cite

phase 2read-only

Print the stable @citekey for one paper while you write.

/ref:cite <pmid>

Arguments

<pmid>
Required. Must already be in the library.

What happens

  1. Reads papers/<pmid>/meta.json and prints @ plus its citekey. The citekey was allocated at /ref:add and does not change, so it is safe to type into a manuscript.

Reads & writes

Reads meta.json. Writes nothing.

Example

output from a scratch library

 /ref:cite 10000001
@rivera2021cortical
 /ref:cite 99999999
error: pmid '99999999' not in library — add it first via /ref:add

Under the hood

cite.py --repo <root> --pmid <pmid>. No subagent.

See also

/ref:export · /ref:add

/ref:export

phase 2

Export a selected set as BibTeX and CSL-JSON into a frozen, named batch.

/ref:export <selector> --batch <label> [--refresh]

Arguments

<selector>
<pmid…>, --project [--question], --screened, --read/--queue, --query [--run], --search, --from-file, refined by --tier/--exclude.
--batch <label>
Required. Directory name under exports/. Reused unless --refresh.
--refresh
Re-resolve, rewrite both files, and report added/removed against the prior freeze.

What happens

  1. An existing batch without --refresh returns reused_frozen_batch and doesn't re-resolve.
  2. Otherwise resolves the selector, builds CSL-JSON from meta.json + authorship.json, and renders BibTeX from that CSL-JSON, so the two formats can't disagree.
  3. Writes both files and a manifest with the selector, PMIDs, citekeys, and resolution report.

Reads & writes

Writes exports/<batch>/{references.bib, references.csl.json, manifest.json} at the library root, even when the selector is --project.

Example

output from a scratch library

 /ref:export --project thesis-ch3 --batch ch3
{
  "status": "created",
  "batch": "ch3",
  "manifest": {
    "batch": "ch3",
    "selector_expression": "--project thesis-ch3",
    "pmids": ["10000001", "10000002"],
    "citekeys": ["rivera2021cortical", "lindqvist2019surface"],
    "resolved_at": "2026-09-15T10:14:30.552480+00:00",
    "report": {
      "count": 2,
      "by_extraction_tier": {"abstract": 2, "full": 0, "unavailable": 0, "missing_record": 0},
      "by_human_verification_state": {"not_yet_tracked": 2},
      "by_retraction_errata_status": {"unknown": 2}
    }
  }
}
# exports/ch3/references.bib
@article{rivera2021cortical,
  title = {Cortical thickness in autistic adults: a cohort study},
  author = {Rivera, Ana and Okafor, Chidi},
  journal = {Example Journal of Neuroimaging},
  year = 2021,
  doi = {10.0000/example.1},
  pmid = {10000001}
}
# second entry trimmed. Running the same command again:
{
  "status": "reused_frozen_batch",
  "batch": "ch3",
  # … same manifest

Under the hood

export.py --repo <root> --batch <label> [selector] [--refresh], using lib_cite.build_exports. No subagent.

See also

/ref:cite · /ref:export-papers · /ref:check-citations --export-bib

/ref:export-papers

phase 3Papers.app

Write selected papers, their PDFs, and optionally notes and tags into a folder ReadCube Papers imports from, without touching its database.

/ref:export-papers <selector> [--to <dir>] [--layout papers|flat] [--pdfs copy|link|none] [--notes[=force]] [--tags <a,b>] [--tags-from <slug>] [--skip-known] [--force] [--collection <name>] [--dry-run] [--batch <label>]

Arguments

<selector>
Any §5c selector.
--to <dir>
Destination. Default: papers_export_dir from ~/.config/ref-manager/config.json, else exports/papers/<batch>/ in the library (and the output says which applied).
--layout papers|flat
papers writes <LastAuthor>/<Journal>-<Year>.pdf (default for the configured folder); flat writes <citekey>.pdf (default otherwise).
--pdfs copy|link|none
Default copy. link symlinks; none omits PDFs and local-url.
--notes[=force]
Push notes.md into the BibTeX note field. Refuses a paper whose note an earlier export already pushed; =force (script: --notes-force) overwrites.
--tags <a,b> · --tags-from <slug>
Fill keywords with explicit tags, or with the project slug for papers in that project.
--skip-known · --force
With a readable Papers database, skip papers it already holds (matched by PMID or DOI); --force exports them anyway.
--collection <name>
Recorded in the manifest only. No Papers collection is created.
--dry-run
Compute and print the plan; write nothing.
--batch <label>
Names the fallback exports/papers/<batch>/ folder.
--refresh
Listed in the command file as "re-resolve even if a manifest exists"; the current script accepts it but always resolves live, so it has no effect.

What happens

  1. Looks for ~/Library/Application Support/Papers/*.db and passes the first match as --papers-db, read-only. If none is found or it can't be read, the export proceeds without duplicate detection; that is expected, not an error.
  2. Resolves the selector, then per paper builds CSL-JSON and adds abstract, volume, issue, pages, and ISSN from the stored raw record when present. Missing ones are listed under field_omissions.
  3. Finds an acquired PDF (via acquisitions.json, falling back to raw/*/source.pdf), allocates its destination path, and copies or links it. Papers without one appear in missing_pdfs.
  4. Never overwrites a file at the destination that an earlier ref-manager export didn't write. Such paths go into foreign_conflicts; under the papers layout, name clashes get -2, -3 suffixes.
  5. Writes a Papers-dialect references.bib (double-braced title and abstract, literal UTF-8, local-url), the manifest, and the written-path index. You then import references.bib through Papers' own UI; this command doesn't drive the app.

Reads & writes

Writes <dest>/{references.bib, manifest.json, .ref-manager-written.json} and the PDFs; records note pushes in exports/papers/note_pushes.json in the library. Reads the Papers database only for duplicates; never writes it, never opens the app.

Example

output from a scratch library

 /ref:export-papers --project thesis-ch3 --to ~/scratch/papers-out --dry-run
{
  "dry_run": true,
  "destination": "…/papers-out",
  "manifest": {
    "selector_expression": "--project thesis-ch3",
    "pmids": ["10000001", "10000002"],
    "citekeys": ["rivera2021cortical", "lindqvist2019surface"],
    "destination": "…/papers-out",
    "destination_reason": "--to",
    "layout": "flat",
    "pdfs_mode": "copy",
    "per_paper": {
      "10000001": {"citekey": "rivera2021cortical", "pdf_path": null, "already_known": false, "pushed_note_hash": null},
      "10000002": {"citekey": "lindqvist2019surface", "pdf_path": null, "already_known": false, "pushed_note_hash": null}
    },
    "missing_pdfs": ["10000001", "10000002"],
    "already_known": [],
    "skipped_known": [],
    "field_omissions": {
      "10000001": ["volume", "issue", "pages", "issn"],
      "10000002": ["volume", "issue", "pages", "issn"]
    },
    "foreign_conflicts": [],
    "duplicate_detection_available": false,
    "duplicate_detection_note": "no --papers-db given",
    "collection": null
  },
  "note_statuses": {}
}
# timestamp trimmed; no PDFs because these papers were never fetched or attached

Under the hood

export_papers.py [selector] --repo <root> [--to …] [--layout …] [--pdfs …] [--notes] [--notes-force] [--tags …] [--tags-from …] [--skip-known] [--force] [--collection …] [--dry-run] [--refresh] [--batch …] [--papers-db …], with papers_snapshot.py for the read-only duplicate check. No subagent.

See also

/ref:export · /ref:attach · /ref:pull-annotations · /ref:open · Papers handoff tutorial