A Claude Code skill that places pre-screened literature citations into a LaTeX or Word manuscript, restyles the citations already in one, or adds verified pinpoint page citations (pincites) to footnoted citations. A single Tkinter launcher selects the mode and its options.
This skill never invents citations. It works only with references the author has already gathered and screened:
- Placement draws exclusively from a user-supplied, pre-screened bibliography.
The inline and footnote modes place citations only from the
.xlsxspreadsheet of screened references you provide. The skill decides where an existing reference belongs; it has no path that conjures a new citation from the model's own memory. - Every reference can be verified against authoritative databases. The
optional verification pass (Phase 1.5) checks each reference against OpenAlex
and CrossRef — with Google Scholar as an additional cross-check when a
SEARCHAPI_API_KEYis set — and flags dangling citation keys and entries it cannot confirm. - Restyling reformats; it does not re-source. When converting citations from one style to another, the skill rewrites formatting only and leaves case citations, legislation, and discursive footnotes untouched, so no substantive citation content is altered or introduced.
- Pincites are verbatim-or-nothing. The optional pincite pass writes a page number only when a verbatim quote from the acquired source text lands on a page map verified against the PDF's own printed folios. No quote match, or no verified map, means no pincite; an unpincited citation is a designed outcome, never an error, and the report says why.
Together these properties address the fabricated-citation risk that attends LLM-assisted writing: the bibliography is the author's, and the skill's role is placement and formatting rather than invention.
The skill has three modes, selected by a run button in the launcher:
- Inline placement — places screened citations as inline
\cite{},\citet{}, or\citep{}commands with a compiledreferences.bibbibliography. For economics, finance, and social-science journals using BibTeX/biber (APA, MLA, Harvard, Chicago author-date, IEEE, Vancouver)..texonly. - Footnote placement — places screened citations as full formatted
\footnote{}(LaTeX) or native Word footnotes (OOXML), with one-click legal and notes style selection (Bluebook, OSCOLA, Chicago notes-bib, APA 7th, McGill) and Id./supra short-form post-processing..texor.docx. - Restyle — converts ALL existing footnote citations from one style to
another, without a spreadsheet.
.texor.docx.
Footnote placement has an optional pincite pass (Phase 5.5, .tex only)
that acquires each cited work's full text, verifies its printed-page map,
locates verbatim supporting quotes, and rewrites the citation to carry the
exact page (557 → 557, 568). The same pass runs standalone on a manuscript
whose citations the author already wrote; see "Pincite Architecture" below.
Given a .tex/.docx manuscript and (for placement) an .xlsx spreadsheet of
screened citations, a placement run maps the manuscript at the paragraph level,
ingests the spreadsheet into BibTeX, plans where each citation belongs using
parallel sub-agents, inserts the citations into a new copy of the manuscript,
and compiles.
-
Python 3.10+
-
openpyxl(pip install openpyxl) for spreadsheet ingestion. -
For
.docxmanuscripts (footnote and restyle modes):python-docx,lxml, andpydantic—pip install python-docx lxml pydantic(seerequirements-docx.txt)..tex-only usage needs no extra dependencies. -
A LaTeX distribution:
pdflatex+bibtex/biberfor inline mode. Footnote, restyle, and pincite runs compile with the manuscript's own engine (detected byscripts/core/tex_engine.pyfrom the author's build log or preamble); the skill never imposes an engine, because a wrong one substitutes fonts silently. -
For the pincite pass:
pymupdfandplaywright(pip install -r requirements-pincite.txt, thenplaywright install chromium);EXA_API_KEYin the environment for book and case discovery; the Firecrawl CLI as an optional bot-wall fallback for case fetches. Scanned PDFs additionally need thepdfskill's GPU OCR runner (LightOnOCR) andeasyocr. -
For the pincite pass's scripted library lane, the institutional access variables below (User scope on Windows, so a fresh shell sees them). Without them the proxied routes fail loudly per work and everything else still runs; open-access, manual-folder and case routes need none of them.
Variable Meaning CITE_PLACEMENT_EZPROXY_PREFIXYour library's EZproxy login prefix, e.g. https://<proxy host>/login?url=CITE_PLACEMENT_IDP_HOSTThe identity-provider host the proxy redirects to for the password step (ADFS) CITE_PLACEMENT_PROXY_AUP_BUTTONThe accessible name of the button on the proxy's acceptable-use page CITE_PLACEMENT_LIBKEY_LIBRARYYour library's LibKey/BrowZine library id (the number in libkey.io/libraries/<id>/)CITE_PLACEMENT_PROXY_USER,CITE_PLACEMENT_PROXY_PASSThe institutional login the scripted lane types; never committed anywhere CITE_PLACEMENT_CONTACT_EMAILPolite-pool contact for Unpaywall/CrossRef (defaults to the proxy user) CITE_PLACEMENT_CACHE_ROOTFull-text cache and browser profile root (default ~/.claude-local/cite-placement) -
Claude Code, invoked via
/cite-placement. -
SEARCHAPI_API_KEYin the environment to enable optional Google Scholar verification (Phase 1.5). Without it, verification falls back to OpenAlex and CrossRef only.
cite-placement/
├── README.md # This file
├── SKILL.md # Router: modes, launcher protocol, commands
├── requirements-docx.txt # Extra deps for .docx support
├── requirements-pincite.txt # Extra deps for the pincite pass (pymupdf, playwright)
├── scripts/
│ ├── launcher.py # Unified Tkinter GUI (3 run buttons + utilities)
│ ├── short_form.py # Phase 6: style-aware short-form substitution;
│ │ # owns the comment-aware citation split (part_spans)
│ ├── pincite_pass.py # Phase 5.5 CLI: collect|acquire|adopt|extract|apply|repin
│ ├── merge_adjacent_footnotes.py # Phase 5 post-proc: merge adjacent \footnote{}
│ ├── reorder_crossrefs.py # Fix supra/Id. after manual edits
│ ├── strip_citations.py # Remove all %CITE-PLACED footnotes
│ ├── migrate_markers.py # Add %CITE-PLACED to legacy footnotes
│ ├── docx_restyle.py # Standalone .docx restyle driver
│ ├── tex_restyle.py # Standalone .tex restyle driver
│ ├── core/
│ │ ├── ingest_citations.py # .xlsx → citations.json + references_new.bib
│ │ ├── verify_citations.py # Phase 1.5 verification (--mode inline|footnotes)
│ │ ├── tex_engine.py # Detect the manuscript's own TeX engine
│ │ └── docx_support/ # OOXML footnote read/write (from word-docx skill)
│ └── pincite/ # Phase 5.5 package (README.md there = full invariants)
│ ├── stages.py # acquire/adopt/extract/apply/repin, journaled commits
│ ├── acquire.py # Acquisition cascade per work type
│ ├── proxy_session.py # Authenticated EZproxy browser session
│ ├── page_map.py # pdf page → printed page map, folio-verified
│ ├── case_pages.py # Star-pagination / paragraph maps for judgments
│ ├── ocr_layer.py # Invisible OCR re-layering of scanned PDFs
│ ├── quotes.py # Verbatim quote matching
│ ├── formatter.py # Style × work-type pincite insert/extract/strip
│ └── audit_round.py # Parenthetical/support audit harvest + apply
├── references/
│ ├── phase-details-inline.md # Inline-mode workflow (5 phases)
│ ├── phase-details-footnotes.md # Footnote-mode workflow (6 phases, incl. 5.5) + Restyle
│ ├── pincite-pass-design.md # Pincite pass: living design spec, playbooks, audit ledgers
│ ├── pincite-pass-plan.md # Pincite pass: original implementation plan
│ ├── audit-round-PROMPT-template.md # Prompt template for audit-round subagents
│ └── styles/ # 5 style packs (.md + .json): bluebook, oscola,
│ # chicago, apa, mcgill
├── tests/ # pytest suite (~2,720 tests): python -m pytest tests
└── tools/
└── cite-restyle/ # Standalone restyle app source (.exe via GitHub Release)
This quick start walks through a restyle run — the most common colleague task. Placement runs follow the same launch flow with a different run button.
npm install -g @anthropic-ai/claude-code
Run claude in your terminal to verify it works. You will need an Anthropic
account or an organisation seat.
Clone the standalone repository straight into your Claude Code skills
directory (Codex users: ~/.agents/skills/cite-placement instead):
git clone https://github.com/kennethkhoocy/cite-placement ~/.claude/skills/cite-placementor install it with its sibling skills through the plugin marketplace:
/plugin marketplace add kennethkhoocy/legal-scholarship-skills
/plugin install legal-scholarship@legal-scholarship-skills
A manual copy works too: put the cite-placement folder under
%USERPROFILE%\.claude\skills\ (Windows) or ~/.claude/skills/ (macOS,
Linux). The folder should contain SKILL.md, scripts/, and references/ at
the top level. No further configuration is needed — Claude Code discovers
skills automatically.
Open a terminal, run claude, and type:
/cite-placement
Claude Code launches a Tkinter GUI window. If it doesn't appear, make sure Python's tkinter is installed (it ships with most Python distributions).
In the GUI:
- Input Manuscript — Browse and select your
.docxor.texfile. - Output File — Auto-fills as
<name>_cited.<ext>; change it if you prefer a different name or location. - Restyle section — Set Current Style to the style your manuscript uses (e.g., Bluebook) and Restyle To to the target style (e.g., OSCOLA).
- Click Restyle.
The .xlsx field is for placement only; leave it empty when restyling.
After the GUI closes, Claude Code automatically:
- Copies your manuscript to the output path (never modifies the original).
- Extracts all footnotes.
- Reformats each footnote from the current style to the target style — Claude Code itself does the reformatting.
- Writes the restyled footnotes back into the output file.
- Reports how many footnotes were converted, skipped, and any errors.
This typically takes 1–3 minutes for a 200-footnote manuscript. A changelog is
saved at <output_dir>/placement/restyle_changelog.json.
Open the output file in Word or your LaTeX editor and check that:
- The citation format matches the target style.
- Cross-references (e.g.,
(n 3)for OSCOLA) point to the correct footnotes. - Discursive footnotes (author commentary, data notes), case citations, and legislation were preserved unchanged.
✓ Journal article citations (author, title, volume, journal, page, year)
✓ Book and working-paper citations
✓ Cross-references (Id./ibid, supra note N / (n N))
✓ Introductory signals (See, See also, Cf. — added or removed per style)
✓ Internal references (Infra/Supra Part → "see Part X below/above")
✓ Author formatting (& vs "and", et al. vs "and others")
✓ Trailing punctuation (period vs no period)
✓ Asterisk/symbol footnote numbering (automatically detected)
✗ Case citations — preserved as-is (jurisdiction-specific)
✗ Legislation — preserved as-is
✗ Discursive footnotes — preserved as-is
One launcher, one config file, three pipelines, one workspace. The launcher
never runs a pipeline: it validates inputs, writes placement/run_config.json
beside the output file, and exits with two status lines. Claude Code, following
SKILL.md, reads the config and routes on its pipeline value; each phase's
instructions live in references/phase-details-*.md, and each deterministic
step is a script under scripts/.
/cite-placement
|
v
scripts/launcher.py (Tkinter) ──writes──> <output_parent>/placement/run_config.json
| |
| stdout: LAUNCHER_STATUS, LAUNCHER_CONFIG_PATH | "pipeline": inline | footnotes | restyle
v v
Claude Code router (SKILL.md) ── reads references/phase-details-{inline,footnotes}.md per phase
|
├─ inline ────> 1 map ─> 1.5 verify* ─> 2 ingest ─> 3 plan (3a/3b/3c) ─> 4 HITL* ─> 5 insert \cite{}
| + merge .bib + pdflatex/bibtex
|
├─ footnotes ─> 1 map ─> 1.5 verify* ─> 2 ingest ─> 3 plan (3a/3b/3c) ─> 4 HITL* ─> 5 insert \footnote{}
| + merge adjacent ─> 5.5 PINCITES* ─> 6 short forms ─> manuscript's own engine
|
└─ restyle ───> copy ─> extract footnotes ─> LLM restyle per section ─> write back
.tex: merge adjacent + short forms + own engine | .docx: OOXML write-back
* optional, gated by a run_config key (verify_citations, auto_approve=false, pincites)
Every phase is either a deterministic script or an LLM step with a fixed contract; the split is deliberate, so that nothing that can be computed is ever guessed.
| Step | Executor | Code |
|---|---|---|
1 Manuscript mapping, footnote classification, % CITE: slot enumeration |
Claude orchestrator (read-only) | .docx footnotes read via scripts/core/docx_support/ |
| 1.5 Verification against OpenAlex, CrossRef, Google Scholar | script | scripts/core/verify_citations.py |
2 Spreadsheet ingestion to citations.json + BibTeX |
script, no LLM | scripts/core/ingest_citations.py |
| 3a Assignment, 3c Consolidation | Claude orchestrator | plan cached on the planned_keys header |
| 3b Per-section anchor drafting | one Opus subagent per section, at most 8 concurrent | style rules injected from references/styles/<id>.md |
| 4 Human review | pause only when auto_approve is false |
|
| 5 Insertion | orchestrator (deterministic from the cached plan) | scripts/merge_adjacent_footnotes.py as the safety net |
| 5.5 Pincites | scripts around one locator fan-out | scripts/pincite_pass.py, scripts/pincite/ (next section) |
| 6 Short forms | script | scripts/short_form.py writes footnote_registry.json |
| Compile | the manuscript's own engine | scripts/core/tex_engine.py detects it |
| Restyle | LLM per footnote/section, deterministic write-back | scripts/tex_restyle.py, scripts/docx_restyle.py |
| Utilities | scripts | strip_citations.py, reorder_crossrefs.py, migrate_markers.py |
All three pipelines share one directory beside the output file. Phase outputs are cached there, which is what makes re-runs incremental.
<output_parent>/placement/
├── run_config.json launcher
├── section_map.md Phase 1
├── existing_citations.json | existing_footnotes.json
├── audit_report.json Phase 1.5 (optional)
├── citations.json, references_new.bib Phase 2
├── assignment.json, section_plans/, placement_plan.md Phase 3
├── footnote_registry.json Phase 6 (used by reorder_crossrefs.py)
├── restyle_changelog.json restyle mode
└── pincite/ Phase 5.5 (see Pincite Architecture)
- Non-destructive. The input manuscript is copied to the output path before anything is written.
%CITE-PLACEDmarker. Every placed\footnote{carries the marker immediately after its brace, one marker line above each citation in a merged footnote. Merge, short-form and reorder rebuilds preserve the marker multiset verbatim, andstrip_citations.pyremoves exactly the marked footnotes.- One citation split.
short_form.part_spansis the single definition of where one citation ends and the next begins inside a footnote: at a top-level;, and at a comment that owns its line when the text before it ends a sentence and the text after it opens a citation or sentence. Phase 6 numbering, pincite collection and pincite application all call it, so the three never disagree about which citation a page belongs to. - Engine preservation. Footnote, restyle and pincite compiles use the manuscript's own engine (build log, else preamble signals). A wrong engine substitutes fonts silently, so parity with the author's PDF (font families, page count) is checked after every compile when that PDF exists.
- Subagent cap. No fleet this skill spawns exceeds 8 concurrent agents; larger work is sharded and queued.
To place new citations from an .xlsx spreadsheet, fill in the .xlsx field,
select a citation style, and click Inline Placement or Footnote
Placement. The pipeline then runs through all phases without pausing,
except at Phase 4 when auto_approve is false.
| Phase | Inline mode | Footnote mode |
|---|---|---|
| 1 | Manuscript mapping + package detection | Manuscript mapping + footnote classification |
| 1.5 | (Optional) verify \cite{} keys vs .bib + APIs |
(Optional) verify footnote citations vs APIs |
| 2 | Ingest .xlsx → JSON + BibTeX |
Ingest .xlsx → JSON + BibTeX |
| 3 | Assign → per-section sub-agents → consolidate | Assign → per-section sub-agents → consolidate |
| 4 | Human-in-the-loop review (if enabled) | Human-in-the-loop review (if enabled) |
| 5 | Insert cites, merge .bib, compile (pdflatex + bibtex/biber) |
Insert footnotes, merge adjacent, compile with the manuscript's own engine |
| 5.5 | — | (Optional) pincite pass |
| 6 | — | Short-form post-processing (Id./supra), carrying pincites into short forms |
See SKILL.md for the launcher protocol, the full command reference, and the
run_config.json shapes; see references/phase-details-inline.md and
references/phase-details-footnotes.md for per-phase detail.
The pincite pass is the most involved part of the skill: five deterministic
stages wrapped around one LLM step, three source types with different
acquisition routes and page models, a folio verifier that must refuse rather
than shift, an identity model that survives manual edits, journaled multi-file
writes, and three maintenance loops. About 2,590 of the skill's 2,720 tests
pin it. This section is the map; scripts/pincite/README.md carries the
invariants in full and references/pincite-pass-design.md the design history.
Prime directive. A pincite exists only when a verbatim quote from the acquired source text lands on a page map verified against the PDF's own printed folios. The LLM proposes a quote; code re-finds it and derives the page; the LLM never chooses a number. No quote match, or no verified map, means no pincite, and the report says why. An unpincited citation is a designed outcome, never an error.
pincited-to-be .tex + placement/citations.json + run_config.json
|
[1] collect deterministic, no network
|
works.json + targets.jsonl (+ unmatched.json; re-keys quotes.jsonl)
|
[2] acquire per-work cascade, proxied routes serialized
articles: cache -> pdf_folder -> OA -> LibKey -> HeinOnline (EZproxy session)
books: cache -> pdf_folder -> OA -> Exa entitled-platform search (EZproxy)
cases: Exa discovery -> plain fetch of top hits -> Firecrawl on bot walls -> identity check
| |
cached PDF / opinion.json unacquired / needs-manual (URL recorded)
+ acquisition.json |
| operator lane: Claude-in-Chrome or pdf_folder -> [2b] adopt
| |
+--------------------+-------------------+
|
[3] extract per-work pages.json
fitz text layer -> page_map (articles, anchored) | book map (folio-inferred) | case_pages (star / paragraph)
scanned or junk-layer PDFs: ocr_layer.py re-layers first (LightOnOCR body + easyocr folio bands)
operator map_hints.json joins the cache signature; PAGE_MAP_VERSION bumps rebuild every map
|
[4] locate LLM fan-out, orchestrator-driven
Opus subagents (<= 8 concurrent, one shard file each) read pages.json + targets.jsonl
and return a verbatim body quote or null -> quotes.jsonl (merged key-idempotently)
|
[5] apply deterministic
quotes.py re-finds each quote in the map -> page(s) derived -> formatter.py splices the
pincite per (style, work type, unit) -> family reconciliation against the live tex ->
journaled commit of .tex + pincites.json + pincite_report.md
|
Phase 6 short_form.py pincites.json is authoritative;
Id./supra carry the pages
maintenance loops beside the flow: repin / retire | audit rounds (audit_round.py) | post-apply triage
| # | Command | Module(s) | In → Out | What it guarantees |
|---|---|---|---|---|
| 1 | pincite_pass.py collect |
pincite_pass.py + short_form.part_spans |
tex + citations.json → works.json, targets.jsonl, unmatched.json |
One target per citation occurrence carrying a work's full title, with its whole-footnote context and hash. Types each work (article / book / chapter / case), parses reporter cites for cases, records an explicit article number. A citation spanning an inline or braced comment is refused loudly. Quotes already located are carried across a citation that only narrowed; orphaned quotes are printed. Records screened works no footnote cites, with the reason, in unmatched.json. |
| 2 | pincite_pass.py acquire |
acquire.py, proxy_session.py |
works → cached PDF or opinion.json + acquisition.json |
Cascade per work type (diagram above). Every cache hit is checked against the meta.json written beside it, because the cache is global and keyed by cite key alone. Proxied routes are serialized four seconds apart. Failures record the final URL for the operator lane. |
| 2b | pincite_pass.py adopt --key K --pdf F |
stages.py |
one hand-fetched PDF → cache + acquisition row | Journaled transaction (.adopt-txn/journal.json); the previous artifact is quarantined, never overwritten. |
| 3 | pincite_pass.py extract |
stages.py, page_map.py, case_pages.py, ocr_layer.py |
PDFs → <cache>/<key>/pages.json ({printed_page: text}) |
Builds and falsifies the printed-page map (next subsection). A map that fails verification yields no pincites for that work, with a named reason. Settles article-number candidates by evidence and writes the answer back to works.json. |
| 4 | orchestrator | prompt template in references/phase-details-footnotes.md |
targets + pages → quotes.jsonl |
Locator fan-out (below). Skips an occurrence only when its stored context_hash still matches; a stale hash means relocate. |
| 5 | pincite_pass.py apply |
stages.py, quotes.py, formatter.py |
quotes → pincited tex, pincites.json, pincite_report.md |
Verbatim re-verification, mechanical page derivation, string surgery with an enforced inverse, loud reconciliation, one journaled commit. |
Acquisition follows one rule set for compliance and one for provenance. The
pipeline never types credentials, never purchases content, and never fetches
SSRN by script (it bot-blocks); paywalled and bot-walled works are reported
unacquired with the URL that stopped them.
- Scripted lane (
proxy_session.py): a persistent Chromium profile through the institutional EZproxy, headless and serialized. LibKey resolves DOIs (library id 0), HeinOnline's OneBoxCitation resolves legal cites, and the EZproxy prefix is the catch-all for publisher URLs. On an MFA prompt it stops withMfaRequired; rerunacquire --headedand approve once. - Operator lane: PDFs fetched in the user's real logged-in browser
(Claude-in-Chrome) for MFA walls, bot-blocked hosts and SSRN, entering
through
adoptor thepdf_folder. The manual folder outranks every network route. - Discovery and fetch tiers: Exa (
EXA_API_KEY) is the only discovery engine, used for books on entitled platforms and for public copies of judgments in any jurisdiction; without a key those routes report themselves skipped. Firecrawl CLI is the fallback fetch when a case candidate's plain HTTP fetch returns a bot wall or a JS shell. There are no site-specific case integrations by design.
A page map is {pdf_page → printed_page}, and every builder returns the same
keys so that quote matching and apply cannot tell the source types apart.
Three builders exist:
- Anchored (articles with a start page,
build_page_map): the offset is known from metadata, and the job is to falsify it. Cover sheets are classified out (a page inside the work's own folio series is never a cover; boilerplate must dominate the page); the span must equal the cited range when the end page is known; every detectable header or footer folio must match the offset model, and one mismatch kills the map. Sparse anchors are normal on OCR scans. - Folio-inferred (books,
build_book_page_map): no metadata window, so the offset comes from the folios alone; pages outside the verified envelope arenulland never receive a pincite. - Case (
case_pages): star pagination (*116) or paragraph numbers ([14]) are the map; a map says which unit it carries, and paragraph pinpoints render bracketed and trail the citation.
Two invariants bind every builder after assignment: printed pages never
decrease along PDF order, and one artifact is paginated on one series. A
second folio series (a reproduced booklet, an appendix restarting at 0) is
refused until the operator's cover_pages or trailing_pages hint removes
it; where the ranges overlap nothing in the artifact says which series is the
work's. A preprint whose own folios contradict the cite is named
version_mismatch, and the remedy is acquire-or-re-cite, never a hint. An
article number in the page slot is settled by evidence, never by width: the
anchored map is attempted first with the cited number, and only when it fails
and the artifact shows no wide folios is the work mapped from its folios and
rendered <number>, at <page>.
placement/pincite/map_hints.json is the operator's only channel into the
map (cover_pages, end_page, drop_trailing_marker_pages,
trailing_pages). Hints add evidence and relax no gate; unknown keys, unknown
works, wrong types and hints on case-backed works are refused before any map
is built. Hints join the cache signature (size:mtime:start:end:sha(hints)),
so a map built under different hints is never reused, and PAGE_MAP_VERSION
(currently 19) rebuilds every cached map on a verifier change.
OCR re-layering engages only when extract reports pages without text or a
map with zero folio anchors. The pdf skill's LightOnOCR runner produces one
Markdown page per true page; easyocr reads the margin bands at their true
positions; ocr_layer.py injects both as invisible text, the body fenced to
the middle of the page so an injected number can never read as a folio. The
body directory is bound to its PDF by page coverage and a sha256 manifest, and
the re-layered PDF must pass the map builder before it replaces the cached
copy.
The only LLM step. The orchestrator groups targets by work, keeps only works
whose map is verified, shards them into at most 8 balanced groups, and spawns
one Opus subagent per shard. Each agent appends to its own
quotes.shard-<i>.jsonl (exactly one writer per file); the shards merge
key-idempotently into quotes.jsonl before apply.
The prompt fixes the evidence rules. The quote must be sliced verbatim from
the cached pages.json text, never retyped and never taken from raw PyMuPDF
output, because every later gate is a raw substring test against that file.
One to three sentences; null when no passage supports the proposition. Front
matter is ineligible (abstract, title block, thanks footnote, keywords,
contents, headnote) and so is the introduction, which announces claims rather
than developing them; conclusions are allowed. A claim stated only in front
matter loses its pincite and the citation stays generic. Confidence is report
metadata for the reviewer, never an insertion filter.
Authority model (protocol v2). The manuscript's main text is hand-reviewed
and determinative; the explanatory parenthetical inside a placed footnote is
generated and unreviewed, a prior to be updated against the article. Locators
derive the proposition from the main-text context alone. Audit rounds judge
support_ok against the main text and parenthetical_ok against the article,
and a round that moves a quote judges the parenthetical against the replacement
passage in the same row.
- Re-verify.
quotes.pynormalizes hyphenation, quotes, dashes and whitespace, then requires the quote as a substring of the mapped text. Not found meansquote-not-found, and nothing is written. - Derive. The page or page range comes from where the match lands; a quote across a page break yields a range, elided per style.
- Format.
formatter.pysplices the pincite by pure string surgery keyed on (style, work type, unit).format_pincite,extract_pinciteandstrip_pinciteare exact inverses, enforced at the moment of writing: a result that would not read back as the pages written, or strip back to the input, is refused asformat-failed. Read-back considers page groups at the citation's own parenthesis depth, so a nested "(citing ...)" citation's page is never mistaken for the outer work's. A (style, work type) pair with no rule is unsupported, not guessed. - Match the live citation. The stored citation is located in the live
footnote on the comment-aware split first and the
;-only split second, so a target stored before the comment-boundary rule still matches and is reportedcitation-commented(re-run collect), nevercitation-drifted. - Reconcile.
pincites.jsonmust never contradict the manuscript. Reconciliation works per family (all targets sharing a footnote and a canonical citation) and keeps an entry only when live pincited copies match the family one-to-one by document order; any mismatch withdraws the whole family aspincite-withdrawn, loudly. Ambiguity refuses, never guesses. - Commit. The tex,
pincites.jsonand the report go down as one journaled commit (commit.journal.jsonunder.commit.lock): staged, backed up, journaled, replaced. TransientPermissionErrors from Dropbox sync locks are retried; recovery rolls back; every read of the pincite directory enters throughopen_pincite_dir, so recovery runs first.
Every target ends the run with one action in pincite_report.md:
inserted, already-pincited, no-quote, quote-not-found, unacquired,
page-map-unverified, format-failed, citation-commented,
citation-drifted, or pincite-withdrawn. Silent survival is a pipeline
failure, the same contract as % CITE: slots.
occurrence_idiscite_key#fnN, with.Mfor a second instance of the same work in one footnote. It is the join key between targets, quotes and pincites.context_hashissha256(citation_raw + "\0" + whole-footnote context)[:12]. It is pincite-blind (page edits do not stale it) but parenthetical-sensitive, so editing any text in a footnote stales every sibling target there;repinrefreshes siblings atomically. The hash is computed over the;-only split, so the comment-boundary rule moved no stored hash.citation_rawstays canonical (un-pincited) even when the tex carries the pincited form, in every unit a style can carry. The formatter's inverses are the only page-string mechanics anywhere in the pass.- A comment line is a boundary, not text. The
%CITE-PLACEDmarker and an author's own-line note bound a citation; a comment trailing live text does not, and that citation is refused rather than guessed at.
-
repin/ retire is the only sanctioned mutation oftargets.jsonlafter a manual edit:repin --occurrence ID [--citation NEW] [--key K]recomputes the live hash, refreshes siblings, re-keys quote rows and re-binds a wrongly attributed occurrence;--retireun-places one occurrence and syncs itspincites.jsonentry in the same commit. Everything else treatstargets.jsonlas append-only truth. -
Audit rounds (
python -m pincite.audit_round collect|apply --config CFG --round-dir DIR, prompt inreferences/audit-round-PROMPT-template.md) are report-only agent fan-outs whose shard output is integrity-gated (schema, newline termination, completeness against the round's manifests, live context hash, replacement quotes verbatim in the artifact) and then applied per occurrence under the same journal: retire support-false placements, splice a revised parenthetical into the citation's final top-level parenthetical group, swap quotes under the fresh hash, or withdraw a stored quote that has no body support. Every finding is revalidated against the live tex at apply time. -
Post-apply triage buckets every not-placed occurrence and gives each bucket a standard fix, never an ad hoc edit:
Bucket Standard fix wrong-version (preprint, other edition, book review) quarantine, re-acquire or adopt, re-extract, relocateverifier-false-negative / map-offset (repository cover sheets) map_hints.json, or a generic verifier fix with aPAGE_MAP_VERSIONbump and a testmetadata-bleed (co-cited works sharing metadata) correct works.json,repin --keyno-folios (web-only, article-numbered) / by-design (letter folios, image-only scans) ledger the accept; a hand folio lives in the tex only Per-manuscript data (hint values, ledger entries) stays in the run directory; every mechanism (subcommand, hint key, verifier relaxation) lands in skill code with a test.
All under <output_parent>/placement/pincite/; full texts are cached outside
the manuscript tree under ~/.claude-local/cite-placement/fulltext/<cite_key>/.
| File | Written by | Contents |
|---|---|---|
works.json |
collect (extract writes back settled article numbers) | Unique works: cite key, type, title, DOI, legal cite, start/end pages |
targets.jsonl |
collect; repin only afterwards |
One row per occurrence: occurrence_id, canonical citation_raw, whole-footnote context, context_hash |
unmatched.json |
collect | Screened works no footnote cites, with the reason |
acquisition.json |
acquire, adopt | Per-work outcome and provenance (route, URL, identity checks) |
map_hints.json |
the operator, never a stage | Cover, trailing, end-page hints per work |
<cache>/<key>/article.pdf or opinion.json, meta.json, pages.json |
acquire, extract | The artifact, its identity guard, and the verified printed-page text map |
quotes.jsonl |
locator agents; re-keyed by collect and repin | Verdicts joined to targets on (occurrence_id, context_hash); apply reads the first row per occurrence |
pincites.json |
apply | Authoritative occurrence-to-page map that Phase 6 reads |
pincite_report.md |
apply | One action per target, reasons, loud withdrawals |
commit.journal.json, .commit.lock, .adopt-txn/ |
apply, adopt | Journal and lock for multi-file commits; recovered on the next open |
Tick Insert pincites in the launcher's placement section (and, optionally,
name a Manual PDF folder holding <cite_key>.pdf files). It is .tex
only and runs as this chain:
python scripts/pincite_pass.py collect --config placement/run_config.json
python scripts/pincite_pass.py acquire --config placement/run_config.json # background + Monitor; --headed on MfaRequired
python scripts/pincite_pass.py extract --config placement/run_config.json
# locator subagents (at most 8 concurrent) write placement/pincite/quotes.shard-*.jsonl, merged to quotes.jsonl
python scripts/pincite_pass.py apply --config placement/run_config.json
# operator lane and maintenance
python scripts/pincite_pass.py adopt --config placement/run_config.json --key <cite_key> --pdf <file>
python scripts/pincite_pass.py repin --config placement/run_config.json --occurrence <id> [--citation "<new>"] [--key <cite_key>] [--retire]
python -m pincite.audit_round collect --config placement/run_config.json --round-dir <dir>
python -m pincite.audit_round apply --config placement/run_config.json --round-dir <dir>
python -m pincite.ocr_layer <in.pdf> <out.pdf> --body-dir <lightonocr per-page dir> [--drop-pages 1,2] [--allow-partial-body]Each subcommand rejects flags that belong to another before touching any
placement file. After upgrading the skill, re-run collect before apply and
read its own lines (re-keyed, refused, orphaned quotes).
The pass has no dependency on the placement pipeline. Every subcommand reads
only three config keys (output_tex, citation_style, pdf_folder),
placement/citations.json, and the manuscript; never %CITE-PLACED markers,
a .bib, or any other placement artifact. A manuscript whose citations the
author already wrote runs the same chain once citations.json is harvested
from its footnotes (recipe: "Standalone pincite mode" in SKILL.md). Existing
short forms are out of scope by construction, because collect targets only
occurrences carrying a work's full title.
scripts/pincite/README.md: the package's own architecture document, with every invariant and the audit ledger summary.references/pincite-pass-design.md: the living design spec, acquisition constants, remediation playbooks, and every hardening round with dates.references/phase-details-footnotes.md, Phase 5.5: the locator protocol and prompt template.
Deterministic ingestion (no LLM) of an .xlsx spreadsheet into BibTeX:
python scripts/core/ingest_citations.py \
--input citations.xlsx \
--output-json citations.json \
--output-bib references_new.bib \
--existing-bib references.bibOptional: --min-score N skips spreadsheet rows whose screening_score is below N (default 0 = no filtering).
Verify existing citations against OpenAlex, CrossRef, and optionally Google Scholar (mode selects the input shape and report schema):
# inline: verify \cite{} keys against the .bib
python scripts/core/verify_citations.py --mode inline \
--citations placement/existing_citations.json \
--bib references.bib \
--output placement/audit_report.json \
--spreadsheet citations.xlsx
# footnotes: verify extracted footnote citations
python scripts/core/verify_citations.py --mode footnotes \
--footnotes placement/existing_footnotes.json \
--output placement/audit_report.json \
--spreadsheet citations.xlsxSet SEARCHAPI_API_KEY in your environment to enable Google Scholar
verification.
For colleagues without a Python or Claude Code setup, the restyle workflow is
also packaged as a standalone Windows executable built from the source in
tools/cite-restyle/ using its bundled PyInstaller .spec. The .exe is
distributed only as a GitHub Release asset and is never committed to the
repository (the built binary is large and platform-specific). A short
README-dist.txt ships alongside the .exe in the release, explaining how to
run it and which styles it supports. To rebuild, run PyInstaller against the
.spec in tools/cite-restyle/ and upload the resulting .exe plus
README-dist.txt to a new release.
- The GUI doesn't open. Confirm tkinter is installed:
python -c "import tkinter". On Ubuntu/Debian:sudo apt install python3-tk. - "Permission denied" on the output file. The output file may be open in Word, or it lives in a Dropbox- or OneDrive-synced folder that has locked it. Close Word and/or write to a local directory.
- Claude Code doesn't recognise
/cite-placement. Check the skill is in the correct directory withSKILL.mdat the top level, then restart Claude Code. - Some footnotes weren't converted. Discursive footnotes, case citations, and legislation are intentionally skipped — check the changelog for details.
- Undefined citation warnings (inline mode). Check
.bibkey spelling and that\bibliography{}/\printbibliographyis present; a missing.bstfile means the relevant LaTeX package needs installing. - A citation was refused as "spans a LaTeX comment" (pincite pass). A
%comment trails live text inside the citation, or sits inside a brace group. Move the comment onto its own line (or delete it) and re-runcollect. - Works reported
unacquired(pincite pass). Expected for paywalled books, bot-walled hosts, and SSRN. Drop<cite_key>.pdfinto the manual PDF folder or runadopt, then re-runextract. - The compiled PDF shows different fonts or a different page count. The
manuscript was compiled with the wrong engine. Run
python scripts/core/tex_engine.py main.texand compile with what it reports.
- Non-destructive: never modifies the original manuscript — always creates a new copy.
- Re-run safe:
.bibmerging avoids duplicates; placement plans are incremental, keyed on theplanned_keysheader. - Agentic Phase 3: parallel sub-agents (one per section) prevent citation drops on large batches.
- Content-agnostic: works for any academic discipline — no hardcoded section names.
- Style-swappable: footnote citation styles change via the GUI without re-running placement decisions; the restyle mode converts existing citations between styles.
- Verbatim-or-nothing pincites: a page is written only from a quote re-verified against a folio-checked page map; every refusal is reported with its reason, and multi-file writes are journaled so a failed run rolls back.
- Engine preservation: footnote, restyle, and pincite compiles use the manuscript's own TeX engine and verify font and page-count parity against the author's PDF when one exists.
This skill consolidates the former cite-placement-inline and cite-placement-footnotes skills into a single router with a shared launcher and a common placement/ workspace.