ref-manager

API · phases 0–11

The programmatic surface

Slash commands are markdown prompts. The deterministic work happens in stdlib-only Python scripts, which the prompts call with explicit flags. This page covers those scripts, the modules they share, the record validators, and the JSON contracts for the two subagents.

Architecture

Each commands/ref-<name>.md file is a prompt that Claude Code runs as /ref:<name>. The prompt parses $ARGUMENTS, reads library_root from ~/.config/ref-manager/config.json (and stops, pointing at /ref:init, if the file is missing), then does some mix of three things:

YOU TYPE /ref:add 31452104 commands/ref-add.md markdown prompt · parses $ARGUMENTS prints each script call before running it ~/.config/ref-manager/config.json library_root → passed as --repo <library_root> missing → fail loudly, point at /ref:init PUBMED MCP TOOLS …PubMed__get_article_metadata …PubMed__get_full_text_article …PubMed__search_articles …PubMed__find_related_articles prefix mcp__claude_ai_ · network lives here PYTHON SCRIPTS python3 "${CLAUDE_PLUGIN_ROOT}/ skills/ref-manager/scripts/<s>.py" --repo · --input-file / --results-file validate → lock → atomic commit stdout JSON or lines · exit 1 on error SUBAGENTS (AGENT TOOL) ref-extractor ×N one per PMID, in parallel ref-synthesizer ×1 one per question · return JSON only MCP results and agent JSON reach scripts as temp files <LIBRARY>/ committed files only scripts write here papers/ projects/ graph/ studies/ … catalog.sqlite · okf/ rebuildable projections catalog.py rebuild · okf_emit.py NOT MCP WebFetch / firecrawl skill: publisher HTML and OA PDFs for /ref:fetch
Scripts never call PubMed or run a subagent. The only HTTP request a script makes itself is fetch.py's Unpaywall lookup; the converters shell out to local tools (pandoc, trafilatura, anydoc). Commands write MCP and agent output to temp files and pass the paths in with flags such as --metadata-file, --input-file, --results-file, --answer-file or --checks-file.

MCP tools the command files reference

ToolCommands
mcp__claude_ai_PubMed__get_article_metadata/ref:add, /ref:extract (retraction status), /ref:audit
mcp__claude_ai_PubMed__get_full_text_article/ref:fetch
mcp__claude_ai_PubMed__search_articles/ref:search-pubmed, /ref:hypothesize
mcp__claude_ai_PubMed__find_related_articles/ref:related

Also named in command files: convert_article_ids (PMCID lookup in /ref:audit --citations); the okf plugin's search_concepts, read_concept and get_neighbors as readers of the generated okf/ (/ref:weave); and the non-MCP WebFetch tool and firecrawl skill in /ref:fetch.

Subagent fan-out

  • ref-extractor: /ref:extract launches one Agent-tool call per PMID, all in one message so they run concurrently, then passes the collected JSON to extract.py --input-file.
  • ref-synthesizer: one invocation per question in /ref:ask and /ref:brief, and over the candidate list in /ref:summarize. The answer is checked with validate_citations.py (ask, brief).

Config file

Location: ~/.config/ref-manager/config.json (init_repo.CONFIG_PATH). init_repo.py writes it atomically and validates it with validate_config. On a fresh init it contains exactly:

{
  "library_root": "/private/tmp/…/scratchpad/concepts-api/lib"
}
KeyRequiredWritten byRead by
library_rootyesinit_repo.pystatus.py; every command prompt resolves --repo from it
unpaywall_emailnonothing; add it by handfetch.py: enables the Unpaywall lookup when a DOI is known
papers_export_dirnonothing; add it by handexport_papers.py: default destination when --to is absent
Heads-up. commands/ref-fetch.md describes unpaywall_email as recorded at /ref:init, but init_repo.py accepts only path and --force and writes only library_root. Re-running init --force rewrites the file with library_root alone, so any keys you added by hand are lost.

Script CLI reference

Every script lives in skills/ref-manager/scripts/, is a PEP 723 single file (requires-python >= 3.11, no dependencies), and is run as python3 "${CLAUDE_PLUGIN_ROOT}/skills/ref-manager/scripts/<script>.py". The usage text below is the real --help output. Subcommands are argparse positional choices, not subparsers, so each script has one help screen.

  • Library. All scripts except init_repo.py, status.py and validate_citations.py take --repo <library_root>. A path that is not a directory gives error: no library at … on stderr and exit 1.
  • Errors. Validation failures (SchemaError, SlugError, SelectorError) print error: … to stderr and exit 1.
  • Batches. add, fetch, attach and extract process each PMID under its own lock and report one line per PMID, so a single failure never blocks the rest of the batch.
  • Selector scripts share the flags [pmids ...] --project --question --screened --read --queue --query --run --search --from-file --study --concept --tier --exclude (semantics).

Library & index

init_repo.py

Create the library directories and log.md, and write config.json. Refuses when a library is already configured unless --force is given.

Called by /ref:init · stdout: plain text

init_repo.py --help
usage: init_repo.py [-h] [--force] path

positional arguments:
  path

options:
  -h, --help  show this help message and exit
  --force

status.py

Report the configured library, the indexed paper count and the project count. It has no argparse CLI and takes no arguments; it reads config.json itself and exits 1 when no library is configured.

Called by /ref:status · stdout: plain text

python3 skills/ref-manager/scripts/status.py

catalog.py

init or rebuild index/catalog.sqlite (tables papers, passages_fts, claims, claims_fts, concepts, relations, catalog_meta) from committed records.

Called by /ref:index · stdout: plain text

catalog.py --help
usage: catalog.py [-h] --repo REPO {rebuild,init}

positional arguments:
  {rebuild,init}

options:
  -h, --help      show this help message and exit
  --repo REPO

Acquire

add.py

RESOLVE and DEDUP: read a metadata envelope (JSON array) and write meta.json, authorship.json, funding.json and raw/<sha256>/response.json. Never calls the network.

Called by /ref:add · stdout: one result line per PMID (plus indented diagnostic: lines)

add.py --help
usage: add.py [-h] --repo REPO --metadata-file METADATA_FILE {add}

positional arguments:
  {add}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --metadata-file METADATA_FILE

fetch.py

Full-text acquisition from a per-PMID envelope (jats_xml, publisher_html, plain_text); makes one direct Unpaywall REST call when unpaywall_email is configured.

Called by /ref:fetch · stdout: one result line per PMID (plus indented diagnostic: lines)

fetch.py --help
usage: fetch.py [-h] --repo REPO --input-file INPUT_FILE

options:
  -h, --help            show this help message and exit
  --repo REPO
  --input-file INPUT_FILE

attach.py

Attach local PDFs as <pmid> <path> pairs after a pdftotext identity check (DOI or title); same hash again is a no-op; conversion goes through convert_pdf.

Called by /ref:attach · stdout: one result line per PMID (plus indented diagnostic: lines)

attach.py --help
usage: attach.py [-h] --repo REPO [--force] pairs [pairs ...]

positional arguments:
  pairs        <pmid> <path> [<pmid> <path> ...]

options:
  -h, --help   show this help message and exit
  --repo REPO
  --force      attach despite a failed identity check

extract.py

Validate ref-extractor output, assign stable claim IDs, commit a version, and promote the tier. Exits 1 if any PMID failed.

Called by /ref:extract · stdout: one result line per PMID (plus indented diagnostic: lines)

extract.py --help
usage: extract.py [-h] --repo REPO --input-file INPUT_FILE
                  [--extractor EXTRACTOR] [--model MODEL]
                  [--prompt-version PROMPT_VERSION]

options:
  -h, --help            show this help message and exit
  --repo REPO
  --input-file INPUT_FILE
  --extractor EXTRACTOR
  --model MODEL
  --prompt-version PROMPT_VERSION

vision.py

request checks the per-figure cache (figure hash + model + prompt); store saves the description the calling agent wrote.

Called by /ref:describe-figure · stdout: JSON document

vision.py --help
usage: vision.py [-h] --repo REPO --pmid PMID [--version VERSION]
                 --figure-id FIGURE_ID --model MODEL --prompt PROMPT
                 [--description DESCRIPTION]
                 {request,store}

positional arguments:
  {request,store}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --pmid PMID
  --version VERSION
  --figure-id FIGURE_ID
  --model MODEL
  --prompt PROMPT
  --description DESCRIPTION

Find

ask_retrieve.py

Retrieval half of /ref:ask: resolve the selector, then collect claim and passage candidates within the candidate and token budgets.

Called by /ref:ask · /ref:brief · /ref:check-citations · stdout: JSON document

ask_retrieve.py --help
usage: ask_retrieve.py [-h] --repo REPO --q Q [--expand [EXPAND ...]]
                       [--candidate-budget CANDIDATE_BUDGET]
                       [--token-budget TOKEN_BUDGET] [--project PROJECT]
                       [--question QUESTION]
                       [--screened {included,excluded,pending}] [--read]
                       [--queue QUEUE] [--query QUERY] [--run RUN]
                       [--search SEARCH] [--from-file FROM_FILE]
                       [--study STUDY] [--concept CONCEPT]
                       [--tier {abstract,full,any}] [--exclude [EXCLUDE ...]]
                       [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --repo REPO
  --q Q                 the research question
  --expand [EXPAND ...]
                        query-expansion terms the calling agent supplied (§5)
  --candidate-budget CANDIDATE_BUDGET
  --token-budget TOKEN_BUDGET
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]

validate_citations.py

Check that every [^pmid] in an answer resolves to a supplied candidate.

Called by /ref:ask · /ref:brief · stdout: JSON document

validate_citations.py --help
usage: validate_citations.py [-h] --answer-file ANSWER_FILE
                             --candidates-file CANDIDATES_FILE

options:
  -h, --help            show this help message and exit
  --answer-file ANSWER_FILE
  --candidates-file CANDIDATES_FILE
                        ask_retrieve.py's candidates JSON

pubmed_query.py

Saved PubMed queries in queries/<slug>.yaml with immutable run histories.

Called by /ref:search-pubmed · /ref:update-queries · stdout: JSON document

pubmed_query.py --help
usage: pubmed_query.py [-h] --repo REPO [--slug SLUG]
                       [--query-text QUERY_TEXT] [--source SOURCE]
                       [--pmids-file PMIDS_FILE] [--create]
                       {new-run,rerun,show,list}

positional arguments:
  {new-run,rerun,show,list}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --slug SLUG
  --query-text QUERY_TEXT
  --source SOURCE
  --pmids-file PMIDS_FILE
  --create

Organize

project.py

Projects, per-project question IDs, and membership (relevance, priority, reading status).

Called by /ref:project · stdout: JSON document

project.py --help
usage: project.py [-h] --repo REPO [--slug SLUG] [--scope SCOPE]
                  [--question-id QUESTION_ID] [--text TEXT] [--pmid PMID]
                  [--relevance RELEVANCE] [--priority PRIORITY]
                  [--reading-status READING_STATUS]
                  {create,add-question,add-paper,show,list}

positional arguments:
  {create,add-question,add-paper,show,list}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --slug SLUG
  --scope SCOPE
  --question-id QUESTION_ID
  --text TEXT
  --pmid PMID
  --relevance RELEVANCE
  --priority PRIORITY
  --reading-status READING_STATUS

queue.py

Set or show reading state, priority and why-saved on a project membership.

Called by /ref:queue · stdout: JSON document

queue.py --help
usage: queue.py [-h] --repo REPO --project PROJECT [--pmid PMID]
                [--status STATUS] [--priority PRIORITY] [--why WHY]
                {set,show}

positional arguments:
  {set,show}

options:
  -h, --help           show this help message and exit
  --repo REPO
  --project PROJECT
  --pmid PMID
  --status STATUS
  --priority PRIORITY
  --why WHY

screen.py

Append a screening decision to screening.jsonl and mirror it onto the membership.

Called by /ref:screen · stdout: JSON document

screen.py --help
usage: screen.py [-h] --repo REPO --project PROJECT [--pmid PMID]
                 [--decision DECISION] [--reason REASON] [--run RUN]
                 {decide,history}

positional arguments:
  {decide,history}

options:
  -h, --help           show this help message and exit
  --repo REPO
  --project PROJECT
  --pmid PMID
  --decision DECISION
  --reason REASON
  --run RUN

note.py

Append to or show papers/<pmid>/notes.md.

Called by /ref:note · stdout: plain text

note.py --help
usage: note.py [-h] --repo REPO --pmid PMID [--text TEXT] {append,show}

positional arguments:
  {append,show}

options:
  -h, --help     show this help message and exit
  --repo REPO
  --pmid PMID
  --text TEXT

verify.py

Correction overlays for claims, grant links, author contributions, person identity and appraisal domains.

Called by /ref:verify · /ref:review · stdout: JSON document

verify.py --help
usage: verify.py [-h] --repo REPO --pmid PMID [--claim-id CLAIM_ID]
                 [--decision DECISION] [--reviewer REVIEWER]
                 [--rationale RATIONALE] [--replacement-file REPLACEMENT_FILE]
                 [--grant GRANT] [--evidence-locator EVIDENCE_LOCATOR]
                 [--award-number AWARD_NUMBER] [--author-index AUTHOR_INDEX]
                 [--flag FLAG] [--evidence-statement EVIDENCE_STATEMENT]
                 [--person PERSON] [--checklist CHECKLIST]
                 [--domain-key DOMAIN_KEY]
                 {review-claim,review-grant-link,review-author-contribution,review-person-identity,review-appraisal,show}

positional arguments:
  {review-claim,review-grant-link,review-author-contribution,review-person-identity,review-appraisal,show}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --pmid PMID
  --claim-id CLAIM_ID
  --decision DECISION
  --reviewer REVIEWER
  --rationale RATIONALE
  --replacement-file REPLACEMENT_FILE
  --grant GRANT
  --evidence-locator EVIDENCE_LOCATOR
  --award-number AWARD_NUMBER
  --author-index AUTHOR_INDEX
  --flag FLAG
  --evidence-statement EVIDENCE_STATEMENT
  --person PERSON
  --checklist CHECKLIST
  --domain-key DOMAIN_KEY

study.py

Create and list study, dataset and method records.

Called by /ref:study · stdout: JSON document

study.py --help
usage: study.py [-h] --repo REPO [--id ID] [--pmid [PMID ...]]
                [--confidence CONFIDENCE] [--evidence EVIDENCE] [--name NAME]
                [--notes NOTES] [--context CONTEXT] [--locator LOCATOR]
                {create-study,create-dataset,create-method,list-studies,list-datasets,list-methods}

positional arguments:
  {create-study,create-dataset,create-method,list-studies,list-datasets,list-methods}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --id ID
  --pmid [PMID ...]
  --confidence CONFIDENCE
  --evidence EVIDENCE
  --name NAME
  --notes NOTES
  --context CONTEXT
  --locator LOCATOR

Compare & write

compare.py

Evidence matrix over a selector, grouped by recorded study. --edit-* writes to the separate user-edit layer.

Called by /ref:compare · stdout: JSON document

compare.py --help
usage: compare.py [-h] [--project PROJECT] [--question QUESTION]
                  [--screened {included,excluded,pending}] [--read]
                  [--queue QUEUE] [--query QUERY] [--run RUN]
                  [--search SEARCH] [--from-file FROM_FILE] [--study STUDY]
                  [--concept CONCEPT] [--tier {abstract,full,any}]
                  [--exclude [EXCLUDE ...]] --repo REPO --batch BATCH
                  [--refresh] [--edit-pmid EDIT_PMID]
                  [--edit-column EDIT_COLUMN] [--edit-value EDIT_VALUE]
                  [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO
  --batch BATCH
  --refresh
  --edit-pmid EDIT_PMID
  --edit-column EDIT_COLUMN
  --edit-value EDIT_VALUE

methods.py

Methods, protocols and datasets for the selected papers, with locators.

Called by /ref:methods · stdout: JSON document

methods.py --help
usage: methods.py [-h] [--project PROJECT] [--question QUESTION]
                  [--screened {included,excluded,pending}] [--read]
                  [--queue QUEUE] [--query QUERY] [--run RUN]
                  [--search SEARCH] [--from-file FROM_FILE] [--study STUDY]
                  [--concept CONCEPT] [--tier {abstract,full,any}]
                  [--exclude [EXCLUDE ...]] --repo REPO
                  [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO

summarize.py

--dump-candidates prints the synthesizer input; --answer-file persists a summary batch; --show reads one back.

Called by /ref:summarize · stdout: JSON document

summarize.py --help
usage: summarize.py [-h] [--project PROJECT] [--question QUESTION]
                    [--screened {included,excluded,pending}] [--read]
                    [--queue QUEUE] [--query QUERY] [--run RUN]
                    [--search SEARCH] [--from-file FROM_FILE] [--study STUDY]
                    [--concept CONCEPT] [--tier {abstract,full,any}]
                    [--exclude [EXCLUDE ...]] --repo REPO [--batch BATCH]
                    [--refresh] [--answer-file ANSWER_FILE]
                    [--coverage-note COVERAGE_NOTE]
                    [--unresolved [UNRESOLVED ...]] [--show]
                    [--dump-candidates]
                    [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO
  --batch BATCH
  --refresh
  --answer-file ANSWER_FILE
  --coverage-note COVERAGE_NOTE
  --unresolved [UNRESOLVED ...]
  --show
  --dump-candidates     resolve the selector and print the candidate evidence
                        list for the ref-synthesizer subagent, without
                        requiring an --answer-file (there's no answer yet at
                        this point -- the calling agent needs candidates
                        BEFORE it can produce one)

appraise.py

Advanced /ref:review: RoB 2 / Newcastle-Ottawa / AMSTAR-2 drafts per PMID plus GRADE certainty.

Called by /ref:review · stdout: JSON document

appraise.py --help
usage: appraise.py [-h] [--project PROJECT] [--question QUESTION]
                   [--screened {included,excluded,pending}] [--read]
                   [--queue QUEUE] [--query QUERY] [--run RUN]
                   [--search SEARCH] [--from-file FROM_FILE] [--study STUDY]
                   [--concept CONCEPT] [--tier {abstract,full,any}]
                   [--exclude [EXCLUDE ...]] --repo REPO --batch BATCH
                   [--refresh]
                   [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO
  --batch BATCH
  --refresh

prisma.py

PRISMA 2020 flow snapshot for a project from saved runs, screening, acquisition and studies.

Called by /ref:review · stdout: JSON document

prisma.py --help
usage: prisma.py [-h] --repo REPO --project PROJECT [--query QUERY]
                 [--refresh]

options:
  -h, --help         show this help message and exit
  --repo REPO
  --project PROJECT
  --query QUERY      query slug, optionally slug:run_id; repeatable
  --refresh

check_citations.py

Persist (check) or read back (show) a paragraph citation check; findings are validated per verdict.

Called by /ref:check-citations · stdout: JSON document

check_citations.py --help
usage: check_citations.py [-h] --repo REPO [--project PROJECT]
                          [--paragraph-file PARAGRAPH_FILE]
                          [--findings-file FINDINGS_FILE]
                          [--candidates-file CANDIDATES_FILE]
                          [--resolution-file RESOLUTION_FILE] [--export-bib]
                          [--id ID]
                          {check,show}

positional arguments:
  {check,show}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --project PROJECT
  --paragraph-file PARAGRAPH_FILE
  --findings-file FINDINGS_FILE
  --candidates-file CANDIDATES_FILE
                        ask_retrieve.py's candidates JSON, after
                        merge_cited_pmid_candidates if the calling command did
                        that
  --resolution-file RESOLUTION_FILE
  --export-bib
  --id ID               check_id, for 'show'

brief.py

Save, edit (a separate user revision layer) or show versioned research briefs.

Called by /ref:brief · stdout: JSON document

brief.py --help
usage: brief.py [-h] --repo REPO --key KEY [--project PROJECT]
                [--question QUESTION] [--answer-file ANSWER_FILE]
                [--evidence-file EVIDENCE_FILE]
                [--resolution-file RESOLUTION_FILE]
                [--unresolved [UNRESOLVED ...]] [--refresh]
                [--revision-file REVISION_FILE]
                {save,edit,show}

positional arguments:
  {save,edit,show}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --key KEY
  --project PROJECT
  --question QUESTION
  --answer-file ANSWER_FILE
  --evidence-file EVIDENCE_FILE
                        ask_retrieve.py's candidates JSON
  --resolution-file RESOLUTION_FILE
  --unresolved [UNRESOLVED ...]
  --refresh
  --revision-file REVISION_FILE

cite.py

Print the stable @citekey for a PMID.

Called by /ref:cite · stdout: plain text

cite.py --help
usage: cite.py [-h] --repo REPO --pmid PMID

options:
  -h, --help   show this help message and exit
  --repo REPO
  --pmid PMID

export.py

BibTeX and CSL-JSON export batch over a selector, into exports/<batch>/.

Called by /ref:export · stdout: JSON document

export.py --help
usage: export.py [-h] [--project PROJECT] [--question QUESTION]
                 [--screened {included,excluded,pending}] [--read]
                 [--queue QUEUE] [--query QUERY] [--run RUN] [--search SEARCH]
                 [--from-file FROM_FILE] [--study STUDY] [--concept CONCEPT]
                 [--tier {abstract,full,any}] [--exclude [EXCLUDE ...]]
                 --repo REPO --batch BATCH [--refresh]
                 [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO
  --batch BATCH
  --refresh

export_papers.py

Papers handoff: references.bib in Papers' dialect, PDF copies, and a manifest. --to falls back to papers_export_dir, then to exports/papers/<batch>/.

Called by /ref:export-papers · stdout: JSON document

export_papers.py --help
usage: export_papers.py [-h] [--project PROJECT] [--question QUESTION]
                        [--screened {included,excluded,pending}] [--read]
                        [--queue QUEUE] [--query QUERY] [--run RUN]
                        [--search SEARCH] [--from-file FROM_FILE]
                        [--study STUDY] [--concept CONCEPT]
                        [--tier {abstract,full,any}] [--exclude [EXCLUDE ...]]
                        --repo REPO [--batch BATCH] [--to TO]
                        [--layout {papers,flat}] [--pdfs {copy,link,none}]
                        [--notes] [--notes-force] [--tags TAGS]
                        [--tags-from TAGS_FROM] [--skip-known] [--force]
                        [--collection COLLECTION] [--dry-run] [--refresh]
                        [--papers-db PAPERS_DB]
                        [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO
  --batch BATCH
  --to TO
  --layout {papers,flat}
  --pdfs {copy,link,none}
  --notes
  --notes-force
  --tags TAGS
  --tags-from TAGS_FROM
  --skip-known
  --force
  --collection COLLECTION
  --dry-run
  --refresh
  --papers-db PAPERS_DB
                        path to a Papers.app sqlite db (tests must point this
                        at a synthetic scratch db, never the real library)

Graph

concept.py

Concept nodes in graph/concepts.jsonl with accumulating aliases and alias provenance.

Called by /ref:concept · /ref:weave · stdout: JSON document

concept.py --help
usage: concept.py [-h] --repo REPO [--id ID] [--name NAME] [--alias ALIAS]
                  [--source SOURCE]
                  {create,find,add-alias,show,list}

positional arguments:
  {create,find,add-alias,show,list}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --id ID
  --name NAME
  --alias ALIAS
  --source SOURCE

relation.py

Typed concept edges: propose/create potential_conflict, create-manual, review, refresh staleness, neighbors.

Called by /ref:weave · /ref:index · stdout: JSON document

relation.py --help
usage: relation.py [-h] --repo REPO [--id ID] [--type TYPE]
                   [--reviewer REVIEWER] [--rationale RATIONALE]
                   [--concept CONCEPT] [--claim-a-file CLAIM_A_FILE]
                   [--claim-b-file CLAIM_B_FILE]
                   [--subject-concept SUBJECT_CONCEPT]
                   [--object-concept OBJECT_CONCEPT]
                   {propose,create,create-manual,review,refresh,show,list,neighbors}

positional arguments:
  {propose,create,create-manual,review,refresh,show,list,neighbors}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --id ID
  --type TYPE
  --reviewer REVIEWER
  --rationale RATIONALE
  --concept CONCEPT
  --claim-a-file CLAIM_A_FILE
  --claim-b-file CLAIM_B_FILE
  --subject-concept SUBJECT_CONCEPT
  --object-concept OBJECT_CONCEPT

okf_emit.py

Regenerate okf/ (concepts, papers, people, grants, index.md, log.md) from the structured sources.

Called by /ref:weave · stdout: JSON document

okf_emit.py --help
usage: okf_emit.py [-h] --repo REPO [--now NOW]

options:
  -h, --help   show this help message and exit
  --repo REPO
  --now NOW

graph_people.py

Regenerate graph/people_relations.jsonl.

Called by /ref:weave · stdout: JSON document

graph_people.py --help
usage: graph_people.py [-h] --repo REPO

options:
  -h, --help   show this help message and exit
  --repo REPO

gaps.py

Structural gap queries: single-study claims, unresolved conflicts, co-mentioned but ungrouped concepts, population/outcome gaps.

Called by /ref:gaps · stdout: JSON document

gaps.py --help
usage: gaps.py [-h] [--project PROJECT] [--question QUESTION]
               [--screened {included,excluded,pending}] [--read]
               [--queue QUEUE] [--query QUERY] [--run RUN] [--search SEARCH]
               [--from-file FROM_FILE] [--study STUDY] [--concept CONCEPT]
               [--tier {abstract,full,any}] [--exclude [EXCLUDE ...]]
               --repo REPO [--intervention-concept INTERVENTION_CONCEPT]
               [--types [{single_study_fragile,unresolved_conflicts,co_mentioned_ungrouped,population_outcome_gap} ...]]
               [pmids ...]

positional arguments:
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO
  --intervention-concept INTERVENTION_CONCEPT
                        required for the population_outcome_gap query
  --types [{single_study_fragile,unresolved_conflicts,co_mentioned_ungrouped,population_outcome_gap} ...]
                        restrict to specific gap types; default runs all
                        applicable

hypothesize.py

Swanson ABC: candidates runs a two-hop traversal; finalize attaches the caller's PubMed check results.

Called by /ref:hypothesize · stdout: JSON document

hypothesize.py --help
usage: hypothesize.py [-h] --repo REPO [--concept CONCEPT]
                      [--checks-file CHECKS_FILE]
                      {candidates,finalize}

positional arguments:
  {candidates,finalize}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --concept CONCEPT
  --checks-file CHECKS_FILE
                        finalize: JSON {concept_c_id: {query, retrieved_at,
                        result_pmids}}

People & reporting

person.py

Researcher profiles plus confirm/reject of an author position.

Called by /ref:person · /ref:discover · stdout: JSON document

person.py --help
usage: person.py [-h] --repo REPO [--slug SLUG] [--name NAME] [--orcid ORCID]
                 [--pmid PMID] [--author-index AUTHOR_INDEX]
                 {create,show,list,confirm-publication,reject-publication}

positional arguments:
  {create,show,list,confirm-publication,reject-publication}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --slug SLUG
  --name NAME
  --orcid ORCID
  --pmid PMID
  --author-index AUTHOR_INDEX

publications.py

Role-filtered publication list for a person, or --coauthors collaborator export for a window.

Called by /ref:publications · stdout: JSON document

publications.py --help
usage: publications.py [-h] --repo REPO --person PERSON [--role ROLE]
                       [--coauthors] [--since SINCE] [--to TO]

options:
  -h, --help       show this help message and exit
  --repo REPO
  --person PERSON
  --role ROLE
  --coauthors
  --since SINCE
  --to TO

grant.py

Grant records and approved award aliases.

Called by /ref:grant · stdout: JSON document

grant.py --help
usage: grant.py [-h] --repo REPO [--slug SLUG] [--funder FUNDER]
                [--award AWARD] [--alias ALIAS] [--title TITLE] [--pi PI]
                [--aims AIMS]
                {create,add-alias,show,list}

positional arguments:
  {create,add-alias,show,list}

options:
  -h, --help            show this help message and exit
  --repo REPO
  --slug SLUG
  --funder FUNDER
  --award AWARD
  --alias ALIAS
  --title TITLE
  --pi PI
  --aims AIMS

discover.py

Persist a manual person/grant PubMed discovery run and its candidates.

Called by /ref:discover · stdout: JSON document

discover.py --help
usage: discover.py [-h] --repo REPO [--person PERSON] [--grant GRANT]
                   --query-text QUERY_TEXT --candidates-file CANDIDATES_FILE

options:
  -h, --help            show this help message and exit
  --repo REPO
  --person PERSON
  --grant GRANT
  --query-text QUERY_TEXT
  --candidates-file CANDIDATES_FILE

report.py

Reproducible person report for a date window; --citations adds PMC-indexed counts.

Called by /ref:report · stdout: JSON document

report.py --help
usage: report.py [-h] --repo REPO --person PERSON --from DATE_FROM
                 --to DATE_TO [--label LABEL] [--citations]
                 [--stale-days STALE_DAYS]

options:
  -h, --help            show this help message and exit
  --repo REPO
  --person PERSON
  --from DATE_FROM
  --to DATE_TO
  --label LABEL
  --citations           include PMC-indexed citing-article counts (D25), never
                        total citations
  --stale-days STALE_DAYS

Maintain

audit.py

retraction and citations apply caller-supplied check results; propagate lists saved artifacts made stale; show-citations reads observations. No selector means the whole library.

Called by /ref:audit · stdout: JSON document

audit.py --help
usage: audit.py [-h] [--project PROJECT] [--question QUESTION]
                [--screened {included,excluded,pending}] [--read]
                [--queue QUEUE] [--query QUERY] [--run RUN] [--search SEARCH]
                [--from-file FROM_FILE] [--study STUDY] [--concept CONCEPT]
                [--tier {abstract,full,any}] [--exclude [EXCLUDE ...]]
                --repo REPO [--results-file RESULTS_FILE]
                [--stale-days STALE_DAYS]
                {retraction,citations,propagate,show-citations} [pmids ...]

positional arguments:
  {retraction,citations,propagate,show-citations}
  pmids                 explicit PMIDs (base case)

options:
  -h, --help            show this help message and exit
  --project PROJECT
  --question QUESTION
  --screened {included,excluded,pending}
  --read
  --queue QUEUE
  --query QUERY
  --run RUN
  --search SEARCH
  --from-file FROM_FILE
  --study STUDY
  --concept CONCEPT
  --tier {abstract,full,any}
  --exclude [EXCLUDE ...]
  --repo REPO
  --results-file RESULTS_FILE
                        JSON array of per-PMID check results from the calling
                        agent
  --stale-days STALE_DAYS

open_in_papers.py

Open a paper's acquired PDF in Papers.app.

Called by /ref:open · stdout: JSON document

open_in_papers.py --help
usage: open_in_papers.py [-h] --repo REPO pmid

positional arguments:
  pmid

options:
  -h, --help   show this help message and exit
  --repo REPO

pull_annotations.py

Upsert one paper's Papers annotations from a read-only snapshot into annotations.json.

Called by /ref:pull-annotations · stdout: JSON document

pull_annotations.py --help
usage: pull_annotations.py [-h] --repo REPO --pmid PMID
                           [--papers-db PAPERS_DB]

options:
  -h, --help            show this help message and exit
  --repo REPO
  --pmid PMID
  --papers-db PAPERS_DB

Library modules

These modules are imported by scripts and have no CLI of their own. Only public names are listed.

lib_atomic.py: atomic writes and locking

atomic_write_bytes(path, data)
Temp file in the same directory, fsync, os.replace.
atomic_write_text(path, text)
UTF-8 wrapper over atomic_write_bytes.
atomic_write_json(path, obj)
Two-space indent, sorted keys, trailing newline.
library_lock(library_root)
Exclusive flock on index/.library.lock: citekeys, slug registries, graph commits.
pmid_lock(library_root, pmid)
Exclusive flock on index/.locks/<pmid>.lock.
commit_version(paper_dir, version_id, write_fn)
Fill versions/.staging-<id>, rename it into place, then point current.json at it. Returns the final path.

lib_ids.py: identifier contract

SlugError
ValueError subclass.
SLUG_RE
^[a-z0-9]+(-[a-z0-9]+)*$; length 1–64 is checked separately.
validate_slug(slug)
Raises SlugError on an illegal slug.
slug_exists(library_root, kind, slug)
Path check for project/query/person/lab/grant; alias-registry check for study/dataset/method/concept.
allocate_slug(library_root, kind, slug)
Refuses a collision, naming the conflicting record; registers registry kinds.
rename_slug(library_root, kind, old, new)
Renames under the library lock and records old → new as an alias.
resolve_slug(library_root, kind, slug)
Follows alias chains to the live slug.
check_question_id(project_yaml, question_id)
Question slugs must be unique within their project.
make_citekey_base(author_lastname, year, first_title_word)
Returns authorYearFirstword, lowercased and alphanumeric only.
allocate_citekey(library_root, author_lastname, year, first_title_word)
Adds a -2, -3 … suffix under the lock and records it in index/citekeys.json.
gen_opaque_id(prefix="")
Prefix + secrets.token_hex(6).

lib_schema.py: record validators

SchemaError(ValueError); validate_config, validate_meta, validate_project, validate_screening_record, validate_claim, validate_correction, validate_study, validate_person, validate_grant, validate_citation_check_finding, validate_concept, validate_relation, validate_retraction_status, validate_citation_observation. Constants: CLAIM_NORMALIZED_FIELDS, STUDY_TYPES, CITATION_CHECK_VERDICTS, RELATION_TYPES. Field-level detail is in Record schemas.

lib_selector.py: selector grammar

SelectorError · NotAvailableError
An empty or invalid selection; --concept raises the latter.
resolve(library_root, *, pmids, project, question, screened, read, queue_state, query, run, search, from_file, study, concept, tier="any", exclude)
Returns {pmids, selector_expression, report}.
selector_expression(**kwargs)
Canonical --flag value string, or <all>.
add_selector_args(ap)
Adds the shared selector flags to an ArgumentParser.
resolve_from_args(library_root, args)
resolve() driven by parsed args.

lib_cite.py: CSL-JSON and BibTeX

to_csl
CSL-JSON generation from library metadata.
csl_to_bibtex_entry
BibTeX rendering from CSL-JSON.
build_exports
Returns CSL entries and BibTeX text for a PMID list; used by export.py and check_citations.py.

lib_verify_link.py: shared by extract.py and verify.py

corrections_path · registry_path
Paths to corrections.json and claim_registry.json.
load_corrections · load_registry
Read the files, with empty defaults.
revalidate_corrections(library_root, pmid)
Flips claim corrections whose target is superseded to pending_review.
apply_reject_to_registry(library_root, pmid, claim_id)
Sets excluded_from_synthesis; the claim is kept.

lib_status_check.py: retraction change detection for saved artifacts

current_status(library_root, pmid)
A PMID's current retraction status string.
diff_status(library_root, prior_status_by_pmid)
Returns a change record for each PMID whose live status differs from the one recorded when the artifact was generated.

Helper modules without a CLI

convert.py
convert_jats, convert_html, convert_plain_text, convert_pdf: one output contract (source.md, figures/, figures.json, status, diagnostics). Used by fetch.py and attach.py.
funding_extract.py
extract_funding_observations(xml_text) returns (observations, state) from JATS funding markup.
papers_snapshot.py
SnapshotUnavailable, take_snapshot (SQLite backup of the live DB), read_items, try_read_items (returns (items, None) or (None, reason)), find_duplicate.

Record schemas

These are the validators in lib_schema.py. They check only the fields listed; writers add more fields than the validators require.

config: validate_config

FieldTypeRule
library_rootstrrequired
papers_export_dir · unpaywall_emailoptional (comment only)

papers/<pmid>/meta.json: validate_meta

FieldTypeRule
pmid · citekey · title · status · checked_atstrrequired (add.py writes status: "active")
extraction_tierstrabstract | full | unavailable
doi · pmcid · journal · year · abstract_availablenot validated; written by add.py
retraction_statusobjectsee validate_retraction_status; written by extract.py and audit.py
full_text · oa_locationnot validated; written by fetch.py and attach.py

meta.retraction_status: validate_retraction_status

FieldTypeRule
statusstrretracted | erratum | none | unknown
source · checked_atstr | nullrequired keys

projects/<slug>/project.yaml: validate_project

FieldTypeRule
slugstrrequired
questionslisteach item needs id (str); IDs unique within the project

screening.jsonl line: validate_screening_record

FieldTypeRule
pmid · reason · timestampstrrequired
decisionstrincluded | excluded | pending
search_runstroptional

versions/<id>/claims.json entry: validate_claim

FieldTypeRule
claim_id · pmid · version_id · evidence_tier · source_hash · locator · evidence_spanstrrequired
study_typestrrct | cohort | case_control | meta_analysis | molecular | imaging | review | mixed | unknown
CLAIM_NORMALIZED_FIELDSanyevery key present ("unknown" allowed): population, intervention, comparator, outcome, timepoint, direction, effect_value, effect_measure, uncertainty_interval, study_design, cohort_identity, adjustment_context

corrections.json entry: validate_correction

FieldTypeRule
correction_id · target_id · reviewer · timestamp · evidence_locatorstrrequired
target_typestrclaim | concept_mapping | person_identity | grant_link | author_contribution | appraisal
decisionstraccept | edit | reject
statusstractive | pending_review

studies/studies.jsonl row: validate_study

FieldTypeRule
study_id · confidence · evidence · review_statestrrequired (study.py uses confidence confirmed|likely|uncertain)
pmidslistrequired

people/<slug>.json: validate_person

FieldTypeRule
slugstrrequired
name_variantslistrequired
orcidoptional

grants/<slug>.json: validate_grant

FieldTypeRule
slug · funder · award_numberstrrequired
approved_aliaseslistrequired (grant.py seeds it with the award number)

check-citations findings.json entry: validate_citation_check_finding

FieldTypeRule
assertion_textstrrequired
verdictstrsupported | overstated | conflicting | insufficient | unavailable
evidencelisteach item needs pmid; must be empty for unavailable and non-empty otherwise
existing_citation_pmid · citation_mismatch · noteoptional

graph/concepts.jsonl row: validate_concept

FieldTypeRule
concept_id · name · created_at · updated_atstrrequired
aliaseslistrequired
alias_provenancedictrequired

graph/relations.jsonl row: validate_relation

FieldTypeRule
relation_id · subject_concept_id · object_concept_id · created_at · updated_atstrrequired
typestrsupports | potential_conflict | contradicts | extends | replicates
supporting_claimslistat least one; each needs pmid and claim_id
source_version_idslistrequired
review_statestrunreviewed | reviewed
rationalestr | nullcontradicts requires reviewed and a non-empty rationale
staleboolrequired

papers/<pmid>/citations.json entry: validate_citation_observation

FieldTypeRule
retrieved_atstralways required
statusstrcheck_failed | no_pmcid marks a non-observation: needs reason (str) and must not have count
source · query · coveragestrrequired for a real observation
countintrequired for a real observation, ≥ 0

Subagent contracts

Both agents return a single JSON object with no prose and no fences. Neither can write to the library: the calling command validates their output and hands it to a script.

agents/ref-extractor.md

Input: pmid, title, abstract, and full_text (markdown from versions/<id>/source.md) or null for an abstract-tier paper. Unknown fields are the literal string "unknown"; "claims": [] is a valid answer.

Output (verbatim)

{
  "pmid": "<string>",
  "study_type": "rct|cohort|case_control|meta_analysis|molecular|imaging|review|mixed|unknown",
  "study_type_confidence": "confident|uncertain",
  "claims": [
    {
      "locator": "<string>",
      "evidence_span": "<direct quote>",
      "evidence_tier": "abstract|full",
      "population": "...", "intervention": "...", "comparator": "...",
      "outcome": "...", "timepoint": "...", "direction": "...",
      "effect_value": "...", "effect_measure": "...",
      "uncertainty_interval": "...", "study_design": "...",
      "cohort_identity": "...", "adjustment_context": "..."
    }
  ]
}

agents/ref-synthesizer.md

Input: question, plus candidates, each with at least pmid, citekey, kind (claim|passage), text, locator, evidence_tier (abstract|full|null) and retraction_status (unknown|none|retracted|erratum). Every [^pmid] must refer to a supplied candidate.

Output (verbatim)

{
  "answer": "<the grounded answer text, in markdown, with [^pmid] citations inline>",
  "coverage_note": "<one or two sentences on evidence tier/status/sufficiency, or null if nothing notable>",
  "unresolved_questions": ["<follow-up the evidence raises but doesn't answer>", "..."]
}

Testing

The phase suites are plain unittest files that build temporary libraries. Run one:

$ python3 skills/ref-manager/scripts/tests/test_phase0.py

Or run all of them, with HOME pointed at a scratch directory so your real ~/.config is never touched:

$ for t in skills/ref-manager/scripts/tests/test_phase*.py; do HOME=$(mktemp -d) python3 "$t" || echo "FAILED: $t"; done

Results at time of writing (2026-09-15, Python 3.14): all 17 suites pass, 220 tests.

SuiteTestsResult
test_phase0.py13OK
test_phase1.py12OK
test_phase2.py10OK
test_phase3_acquire.py20OK
test_phase3_papers.py9OK
test_phase4_annotate.py13OK
test_phase4_extract.py18OK
test_phase5_compare.py18OK
test_phase5_prisma.py7OK
test_phase6_ask.py13OK
test_phase7_checkcitations.py12OK
test_phase8_graph.py17OK
test_phase8_okf.py7OK
test_phase9_gaps.py14OK
test_phase9_related.py7OK
test_phase10_summarize_review.py15OK
test_phase11_audit.py15OK

Fixtures live in skills/ref-manager/scripts/tests/fixtures/ (sample.html, sample.jats.xml). Tests that touch Papers.app use --papers-db with a synthetic scratch database, never the real library.