ref-manager

Workflows · phases 0–11

How the pieces move

Nine diagrams of what the commands actually do, drawn from the scripts under skills/ref-manager/scripts/. Command names in the diagrams link to their reference entries. Wide figures scroll sideways on small screens.

1 · Command map

The 40 commands fall into seven groups, and every group works through the same library folder on disk. Acquire fills it. Organize adds your project state and review decisions. The remaining groups mostly read from it and write their own derived outputs next to it.

acquire init add fetch attach extract describe-figure WRITES papers/<pmid>/ meta.json · raw/ versions · current.json claim_registry.json figures.json find search ask search-pubmed update-queries related READS catalog.sqlite WRITES queries/<slug>.yaml related.json organize project queue screen note verify study WRITES projects/: papers.yaml screening.jsonl corrections.json notes.md · studies/ maintain audit index status open pull-annotations WRITES retraction status citations.json catalog (rebuild) annotations.json library root (files) papers/<pmid>/ projects/ · queries/ index/catalog.sqlite graph/ · people/ · grants/ tables/ · summaries/ · briefs/ people person publications grant discover report READS authorship.json funding.json WRITES people/ grants/ reports/ write compare methods summarize review check-citations brief cite export export-papers READS active claims · selector sets · retrieval candidates WRITES tables/ · summaries/ · briefs/ · reviews/ · checks/ · exports/ graph weave concept gaps hypothesize READS active, non-excluded claims (claim_registry.json) WRITES graph/concepts.jsonl · relations.jsonl · okf/
library files, the single source of truth teal arrow: the group mainly writes into the library dark arrow: the group mainly reads from it
All 40 commands, grouped as in the command reference. The main dependencies between groups: /ref:extract claims feed find, write and graph. project, screening and saved-query state feeds the selectors used by write. /ref:verify rejections remove claims from retrieval and the graph. /ref:audit status changes show up when write artifacts are refreshed.

2 · Ingest & commit

/ref:add never overwrites a paper and never lets one paper's failure block another. A per-PMID lock serializes all writes to a single paper. A library-wide lock is taken only for the short moment a citekey is allocated. Later stages add versioned content through one staging-then-swap routine.

/ref:add <pmid…> get_article_metadata one MCP call, all PMIDs envelope JSON pmid · title · abstract · authors doi · pmcid · grants, order kept add.py add --metadata-file records are processed one at a time; a failure is reported for that PMID only and never blocks the rest pmid_lock · index/.locks/<pmid>.lock meta.json exists? dedup by PMID only title or authors? both empty → stop allocate_citekey .library.lock -2, -3 on collision validate_meta lib_schema atomic writes (temp file + os.replace) raw/<sha256>/response.json · meta.json authorship.json · funding.json abstract null → extraction_tier: unavailable yes no error already_present nothing rewritten failed no metadata returned failed schema error, this PMID only added citekey · extraction_tier abstract | unavailable after the lock DOI shared with another PMID → warning, no merge LATER COMMITS · FETCH · ATTACH · EXTRACT (lib_atomic.commit_version) versions/.staging-<id>/ created under pmid_lock write_fn(staging) source.md, figures.json, or claims.json + manifest.json os.replace staging → versions/<id>/ atomic rename current.json {"version": <id>} atomic write, the last step write_fn raises staging removed, pointer kept catalog.sqlite is not written at commit: it is rebuilt from current.json pointers (/ref:index --rebuild, and before each /ref:ask)
per-PMID lock / committed result library lock, or a failure branch note or side check
/ref:add only resolves and deduplicates. It writes meta.json, authorship.json and funding.json directly, with atomic replacement. Versioned content comes later from /ref:fetch, /ref:attach and /ref:extract, each through commit_version. Readers only follow current.json, so a half-written staging directory is never seen. That is also why the SQLite catalog can always be thrown away and rebuilt (/ref:index --rebuild).

3 · Full-text acquisition

fetch.py tries each paper's sources in a fixed order and stops at the first one that applies. The PubMed and publisher lookups are done by the command itself; the script calls only the Unpaywall API. Local PDFs go through /ref:attach, which refuses a file unless it can match the file to the paper.

/ref:fetch <pmid…> first rung that applies wins 1 jats_xml genuine JATS markup only 2 DOI + unpaywall_email Unpaywall best_oa_location 3 publisher_html fetched by the command 4 plain_text e.g. the MCP full_text string 5 nothing usable every input null / no OA URL convert_jats pandoc; XML check first oa_location_found URL recorded, stops here convert_html trafilatura (uv worker) convert_plain_text raw-text; no structure abstract_only full_text: false · expected commit_version raw/<sha256>/source.<kind> preserved before conversion versions/<id>/source.md (+ figures.json, manifest) → current.json acquired meta.full_text: true JATS: funding.json appended failed conversion status ≠ ok staging discarded /ref:attach <pmid> <path> [--force] same bytes already stored? raw/<sha256>/source.pdf duplicate_noop nothing written identity check (pdftotext) first 2 pages contain the DOI, or the first 8 title words refused no match and no --force store raw/<sha256>/source.pdf attachment.json: verified, forced commit_version · convert_pdf anydoc; if it is unavailable the version commits with that status attached full_text: true only when ok yes new bytes match, or --force FIGURES figures.json (per version) JATS <fig>: label, caption, locator HTML image refs: locator only asset_available: false (no bytes) /ref:describe-figure vision.py request cache key: sha256 + model + prompt on demand, never automatic asset_unavailable · figure_not_found cache_hit → stored description needs_description → agent looks, then vision.py store (model_interpretation)
converts and commits a version stops without full text, or needs you terminal no-op / not a failure
/ref:fetch records an Unpaywall hit (oa_location_found) and stops there. It does not download the PDF: you download it and hand it to /ref:attach. The live PubMed full-text tool returns plain text, so it belongs in plain_text, not jats_xml. Section structure and figures are then lost, and a diagnostic says so. PDF conversion produces no figures.json entries. /ref:describe-figure can only describe figures whose image bytes and hash exist; its output is labelled as a model interpretation and stored next to the caption, never in place of it.

4 · Extraction fan-out

/ref:extract starts one ref-extractor subagent per PMID, all in parallel. extract.py never sees the paper itself: it validates what each agent returned, assigns stable claim IDs and commits each paper on its own.

INPUT & TIER SUBAGENT VALIDATE COMMIT /ref:extract <pmid…> reads committed source.md or abstract PMID A source.md at current.json evidence_tier: full ref-extractor one subagent, parallel → study_type, claims[] as JSON extract.py validate every claim before the registry is touched commit_version stable claim_ids claims.json + manifest claim_registry.json PMID B no committed full text evidence_tier: abstract ref-extractor reads the abstract only never blocks on missing full text extract.py schema ok commit_version own version, own lock independent of A and C PMID C full text or abstract ref-extractor returns a malformed claim extract.py SchemaError failed this PMID only; no registry change; A and B still commit CLAIM IDS ACROSS RERUNS · per locator, against the claims active before the run same content hash → same claim_id; review decisions kept changed hash, exactly one prior claim → new id; old claim gets superseded_by new locator, or several candidates → new id; no supersession asserted extraction_tier only moves up (unavailable → abstract → full). After each commit, corrections that target a superseded claim flip to pending_review.
subagent judgment (LLM) deterministic script committed version per-PMID failure
/ref:extract sets the evidence tier from what is committed: full if current.json points at a source.md, abstract otherwise. It also re-checks retraction status for each paper. When two candidates are ambiguous, a new ID is minted rather than guessing a supersession. A missed supersession can be fixed with /ref:verify; a wrong one would silently drop a real claim.

5 · Human review overlays

Your review decisions live in corrections.json, outside the regeneration path. Re-running extraction never erases them. If the evidence a decision points at changes, the decision is flagged pending_review instead of being silently reapplied or dropped.

/ref:verify claim accept|edit|reject grant-link accept|reject author-contribution person-identity show <pmid> verify.py also has review-appraisal corrections.json papers/<pmid>/, append a record target_type · target_id decision · rationale reviewer · timestamp evidence_locator status: active | pending_review side effects of the same call claim reject → claim_registry.json excluded_from_synthesis: true (kept, never deleted) grant-link → grants/<slug>.json publication_links author-contribution → authorship.json flags refused without an explicit evidence statement person-identity → person.py confirm / reject claim accept / edit → recorded in corrections.json only WHAT HAPPENS LATER /ref:extract rerun same content hash at the locator same claim_id evidence unchanged decision kept correction stays active /ref:extract rerun changed content, one prior claim new claim_id old claim marked superseded status: pending_review never silently reapplied or dropped claim rejected excluded_from_synthesis: true ask retrieval · gaps · relations use only active, non-excluded claims /ref:brief · /ref:summarize --refresh list it under withdrawn_evidence
authoritative review record / decision preserved evidence changed, needs your re-review surfaced on refresh
/ref:verify show re-checks the records against the claim registry before printing them, and so does every /ref:extract commit, which is how superseded targets become pending_review. Two limits in phases 0–11. First, a claim edit stores its replacement value, but no reader applies it to the claim; only reject changes what synthesis sees. Second, /ref:compare cells and a fresh /ref:summarize filter by status: active only, so a rejected claim can still appear there.

6 · Selector resolution

Every set-based command takes the same selector grammar from lib_selector.py. It follows three steps: resolve the set, report what is in it, then do the work. The resolved PMID list is frozen into the artifact's manifest, so its membership can only change when you run an explicit --refresh, which then shows what changed.

selector expression BASE · first one given wins <pmid…> · --from-file --project · --query [--run] --study · --search NARROW · needs --project --screened included|excluded|pending --read · --queue <state> REFINE --exclude · --tier abstract|full|any --concept → raises "not available" resolved PMIDs deduplicated, sorted empty → SelectorError naming the expression, never a silent empty set report (before any work) count by_extraction_tier by_retraction_errata_status by_human_verification_state verification state is always not_yet_tracked frozen manifest.json selector_expression · pmids · resolved_at · report rerun without --refresh → reused_frozen_* (membership never shifts silently) --refresh re-resolve /ref:compare --refresh added · removed changed_evidence_cells retraction_status_changes /ref:summarize --refresh added_pmids · removed_pmids withdrawn_evidence retraction_status_changes /ref:brief --refresh added_support_claim_ids · new_pmids withdrawn_evidence retraction_status_changes
what you type frozen record explicit refresh and its diff
One grammar is shared by /ref:compare, /ref:summarize, /ref:brief, /ref:export, /ref:ask and /ref:audit. Audit is the one exception to the rule that an empty selector is an error: with no selector it sweeps the whole library. /ref:brief stores the list as pmids_at_resolution. The tier report counts abstract, full, unavailable and missing_record. A bare PMID list and an equivalent --project selector produce identical comparison rows.

7 · Grounded answer loop

/ref:ask works without embeddings. Claude suggests extra search terms, a lexical FTS5 search fills a fixed budget of evidence, one synthesizer writes the answer, and a script checks that every citation points at evidence the synthesizer was actually given.

/ref:ask "<question>" EXPANSION Claude picks 2–4 MeSH terms, synonyms, aliases --expand <term…> + optional selector ask_retrieve.py 1 catalog.rebuild() first 2 FTS5 OR-match over claims_fts (active, not excluded) + passages_fts 3 keep selector PMIDs only 4 dedupe: claims beat passages 5 round-robin across PMIDs 6 budget: 30 candidates, 6000 tokens (about 4 characters per token) ref-synthesizer one subagent, given every candidate at once RETURNS answer coverage_note unresolved_questions validate_citations.py each [^pmid] must be in the candidate set all_resolved unresolved: […] reported to you, never silently dropped or fixed insufficient_coverage: true say so plainly; no padded synthesis answer · coverage note · unresolved questions with evidence tier and retraction status; /ref:ask saves nothing /ref:brief "<question>" --key <label> [--project <slug>] [--refresh | --show | --edit] same loop as /ref:ask expand → retrieve → synthesize → validate brief.py save briefs/<key>/<snapshot_id>/ answer.md · evidence.json · manifest.json latest.json points at the snapshot key already saved? no --refresh → reused_frozen_brief --refresh → new snapshot + diff the prior snapshot stays on disk --edit <text> → edits.json kept while the evidence is unchanged; stale: true when evidence_hash changes REFRESH DIFF added_support_claim_ids · new_pmids withdrawn_evidence retraction_status_changes
deterministic script subagent, or a result that needs attention presented / persisted result
/ref:ask answers once and keeps nothing. /ref:brief wraps the same four steps and saves the result as a snapshot under projects/<slug>/briefs/<key>/, or under briefs/<key>/ at the library root when you give no project. Refreshing is always explicit: nothing re-runs on a schedule. Citation validation only checks that each cited PMID was among the candidates given to the synthesizer. Whether the evidence actually supports the sentence is checked by /ref:check-citations.

8 · Reading & screening state

Reading and screening state belong to one paper's membership in one project, not to the paper itself. Only /ref:project, /ref:queue and /ref:screen can change them. Fetching full text or extracting claims never marks a paper as read.

reading_status on the membership record in projects/<slug>/papers.yaml set by /ref:project add-paper --reading-status, or /ref:queue set --status not set (null) to_screen to_read reading read typical path; any of the four states can be set directly, no order is enforced screening per project · /ref:screen decide --decision … --reason "…" [--run <query-run>] /ref:screen reason required; adds the PMID as a member if needed pending included excluded screening.jsonl append-only history; latest copied to membership → --screened selector /ref:add · /ref:fetch · /ref:attach · /ref:extract acquisition and extraction write papers/<pmid>/ only never set reading_status or screening state lives on the project membership, so one PMID can be read in one project and to_screen in another
reading state / included excluded, or a write that is blocked unset, or a direct jump
/ref:project add-paper can set a starting state, and /ref:queue set moves a paper to any of the four states, together with priority and a why-saved note. /ref:screen logs every decision and copies the latest onto the membership. These records feed the --read, --queue <state> and --screened selectors (figure 6), and the PRISMA counts in /ref:review --prisma.

9 · Library maintenance

Maintenance never erases what the library already knows. A failed retraction check keeps the previous status. Citation counts are only ever appended. The SQLite catalog is a projection of the files that you can rebuild at any time.

RETRACTION & ERRATA /ref:audit [selector] no selector = whole library PubMed MCP metadata: article_types audit.py retraction one PMID at a time, under its pmid_lock result or error? checked meta.json retraction_status changed: true | false check_failed prior status retained, never downgraded audit.py propagate changed_pmids → potentially_stale_artifacts you run --refresh brief · compare · summarize retraction_status_changes CITATION OBSERVATIONS · --citations /ref:audit --citations needs a real cited-by count source; none is wired in yet per-PMID input observation no_pmcid error none of the three → refused citations.json append-only, every entry dated: {count, source, query, coverage} {status: no_pmcid} {status: check_failed} show-citations latest real count only (markers skipped) stale after --stale-days (default 180) INDEX & STATUS /ref:index --rebuild idempotent; safe to run at any time catalog.py rebuild delete and recreate catalog.sqlite papers · passages_fts claims · claims_fts reads current.json targets only; staging directories ignored relation.py refresh if graph/relations.jsonl superseded / excluded evidence → stale: true /ref:status read-only library path, papers indexed, project count
recorded or appended failure that keeps prior data your command / your decision
When /ref:audit changes a paper's status, it does not rewrite saved artifacts. propagate lists the tables/, summaries/ and briefs/ entries (including those inside projects) that cite that paper, and each one stays frozen until you refresh it. --citations is fully built, but no connected tool provides a real cited-by count yet, so the command says so rather than recording word-similarity results as citations. /ref:index --rebuild runs relation.py refresh, which flags edges for re-review and never clears a review decision. /ref:status reads only what the catalog currently holds.