diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index de64e77..94558e8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,8 +23,7 @@ "./skills/investigate", "./skills/critique", "./skills/synthesize", - "./skills/search-arxiv", - "./skills/search-iacr" + "./skills/search-paper" ] } ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dd4d2c..86b3cdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: Install Python dependencies run: pip install pytest arxiv requests beautifulsoup4 - name: Run integration tests (search scripts against live APIs) - run: pytest tests/test_search_arxiv.py tests/test_search_iacr.py -v + run: pytest tests/test_search_paper.py -v npx-skills-discovery: runs-on: ubuntu-latest @@ -60,7 +60,7 @@ jobs: exit 1 fi ls -la "$target" - expected="reaper clarify-goal analyze-paper review-literature formalize-problem brainstorm investigate critique synthesize search-arxiv search-iacr" + expected="reaper clarify-goal analyze-paper review-literature formalize-problem brainstorm investigate critique synthesize search-paper" missing=0 for skill in $expected; do if [ ! -f "$target/$skill/SKILL.md" ]; then @@ -69,7 +69,7 @@ jobs: fi done # Co-located Python scripts must travel with their skill dirs - for script in search-arxiv/search_arxiv.py search-iacr/search_iacr.py; do + for script in search-paper/arxiv.py search-paper/iacr.py search-paper/semantic_scholar.py search-paper/dblp.py search-paper/openalex.py; do if [ ! -f "$target/$script" ]; then echo "::error::Missing required asset: $target/$script" missing=$((missing + 1)) diff --git a/CLAUDE.md b/CLAUDE.md index f07b2de..e6aec0c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,11 +4,11 @@ AI-native scientific research pipeline distributed as a host-agnostic skills pac ## Project structure -- `skills/` — 11 composable skills (each has a `SKILL.md` defining its behavior; the `/` form is the canonical display convention used in all user-facing docs) +- `skills/` — 10 composable skills (each has a `SKILL.md` defining its behavior; the `/` form is the canonical display convention used in all user-facing docs) - `/reaper` — Main orchestrator that chains all other skills - `/clarify-goal` — Interactive goal clarification (asks user targeted questions before pipeline runs) - `/analyze-paper`, `/review-literature`, `/formalize-problem`, `/brainstorm`, `/investigate`, `/critique`, `/synthesize` — Pipeline stages - - `/search-arxiv`, `/search-iacr` — Academic search via Python scripts + - `/search-paper` — Academic search + citation graph + venue resolution. Bundles five Python drivers (`arxiv.py`, `iacr.py`, `semantic_scholar.py`, `dblp.py`, `openalex.py`); the `SKILL.md` itself orchestrates the layered venue lookup. - `tests/` — Python tests for skill structure and search scripts - `evals/` — Test cases with quality criteria (`evals.json`) - `dev/` — Development docs including `ROADMAP.md` (full methodology and design) @@ -32,7 +32,7 @@ pip install arxiv requests beautifulsoup4 - Runtime state goes in `reaper-workspace/` (gitignored). Never commit workspace artifacts. - The six methodology principles (separation of concerns, fixed evaluation signal, structured results log, keep-or-discard loop, never stop, clarity and simplicity) govern how skills behave. - Domain-specific content (impossibility results, trust model checklists, venue tiers, definitional standards) lives in `skills/reaper/references/`, not inline in skills. Skills reference these files but remain domain-agnostic — the reference files can be swapped for a different research domain. -- Python scripts live alongside the skill that uses them (e.g., `skills/search-arxiv/search_arxiv.py`). +- Python scripts live alongside the skill that uses them (e.g., `skills/search-paper/arxiv.py`). - No JavaScript/TypeScript in this project — it's `SKILL.md` files + Python only. - The license is Apache-2.0. Any plugin manifest that references a license field must say `"Apache-2.0"`. - When cutting a release tag, the tag message should summarize changes since the last tag (use `git log ..HEAD`). diff --git a/README.md b/README.md index 24ef248..5966508 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ How you invoke a skill depends on the host agent. The `/` form above is t - **Autonomous multi-stage pipeline** — goal clarification, paper analysis, literature review, hypothesis formalization, parallel investigation, critique, and synthesis all chain automatically - **Parallel investigation with keep-or-discard discipline** — multiple hypotheses are investigated concurrently; only genuine progress advances the working state, while dead ends stay logged -- **Built-in academic search** — arXiv and IACR ePrint search with PDF download and citation graph tracing +- **Built-in academic search** — paper search, PDF download, citation graph tracing, and venue resolution across arXiv, IACR ePrint, Semantic Scholar, DBLP, and OpenAlex - **Domain-agnostic design** — ships with cryptography and distributed systems references, but swap the reference files to adapt to any research domain - **Multi-model AI consultation** — optionally consult Codex, Gemini, DeepSeek, or local models for a second opinion at every pipeline stage - **Composable skills** — each pipeline stage is an independent skill you can run standalone @@ -71,8 +71,7 @@ Each skill can be used independently or composed by the orchestrator. Invoke by | `/investigate` | Run investigation cycles with keep-or-discard discipline | | `/critique` | Provide critique via human feedback, Codex consultation, or self-review (can trigger more investigation) | | `/synthesize` | Generate a structured research report from investigation results | -| `/search-arxiv` | Search arXiv papers, download PDFs, and trace citation graphs | -| `/search-iacr` | Search IACR ePrint archive for cryptography papers | +| `/search-paper` | Find papers, download PDFs, trace citation graphs, and resolve publication venues across arXiv, IACR ePrint, Semantic Scholar, DBLP, and OpenAlex | > The `/` form is the canonical display convention used throughout these docs. Slash-command hosts (Claude Code) invoke them directly that way (e.g. `/clarify-goal`). Auto-discovery hosts (Cursor, Codex CLI, Cline, Continue, Gemini CLI, Copilot, Windsurf, …) invoke them by the bare skill name — drop the leading `/` when asking the agent to run a skill. @@ -90,7 +89,7 @@ pip install arxiv requests beautifulsoup4 ### Install via `npx skills` (recommended — works on 45+ agents) -Reaper is distributed as standard `SKILL.md` folders. The cross-agent installer [`vercel-labs/skills`](https://github.com/vercel-labs/skills) shallow-clones this repository and copies all 11 skill directories — including Python scripts and reference files — into your agent's conventional skills folder. +Reaper is distributed as standard `SKILL.md` folders. The cross-agent installer [`vercel-labs/skills`](https://github.com/vercel-labs/skills) shallow-clones this repository and copies all 10 skill directories — including Python scripts and reference files — into your agent's conventional skills folder. ```bash # Latest from the default branch @@ -204,7 +203,7 @@ See [`dev/ROADMAP.md`](dev/ROADMAP.md) for the full methodology and development See [`dev/ROADMAP.md`](dev/ROADMAP.md) for the full roadmap. - **Horizon 1 (The Pipeline)**: Core skills, orchestrator, and eval framework — *complete; LaTeX report output planned* -- **Horizon 2 (The Library)**: arXiv/ePrint search via Python scripts + citation graph — *complete* +- **Horizon 2 (The Library)**: arXiv/ePrint search via Python scripts + citation graph + venue resolution (Semantic Scholar / DBLP / OpenAlex) — *complete* - **Horizon 3 (The Committee)**: Multi-model critique via the `/critique` skill's `--codex` mode — *Codex complete, Gemini/DeepSeek/local planned* - **Horizon 3.5 (The Polyglot)**: Cross-agent distribution via `npx skills` and host-agnostic skill prose — *complete; per-host orchestration polish ongoing* - **Horizon 4 (The Academy)**: Broader topic search (Scholar/DBLP), author-centric and venue-centric search — *planned* diff --git a/dev/ROADMAP.md b/dev/ROADMAP.md index ec144b8..7d4603c 100644 --- a/dev/ROADMAP.md +++ b/dev/ROADMAP.md @@ -6,7 +6,7 @@ ### Value Proposition -Today, AI can answer questions about papers. Reaper goes further: it *does research*. Given a paper on a new consensus protocol and the goal "determine if this is secure under asynchrony," Reaper will read the paper, search arXiv and IACR ePrint for related work, formalize the problem, attempt a proof or construct a counterexample, seek feedback from other AI models, and produce a structured research report with full reasoning traces. +Today, AI can answer questions about papers. Reaper goes further: it *does research*. Given a paper on a new consensus protocol and the goal "determine if this is secure under asynchrony," Reaper will read the paper, search academic sources (arXiv, IACR ePrint, Semantic Scholar, DBLP, OpenAlex) for related work, formalize the problem, attempt a proof or construct a counterexample, seek feedback from other AI models, and produce a structured research report with full reasoning traces. The key insight: research is a *pipeline* of distinct, composable activities (read, search, formalize, analyze, verify, synthesize), not a monolithic task. Reaper decomposes this pipeline into individual skills that can be invoked independently or orchestrated together, and leverages parallel subagents and multi-model feedback to approximate the quality of collaborative human research. @@ -133,12 +133,13 @@ reaper/ │ ├── investigate/SKILL.md # Stage 3: investigate (proof/analysis cycles) │ ├── critique/SKILL.md # Stage 3 sub-step: human / external-model / self review │ ├── synthesize/SKILL.md # Stage 4: synthesize (report generation) -│ ├── search-arxiv/ # Crypto/CS topic search via arXiv -│ │ ├── SKILL.md -│ │ └── search_arxiv.py # arXiv API + Semantic Scholar citations -│ └── search-iacr/ # Crypto-specific IACR ePrint search -│ ├── SKILL.md -│ └── search_iacr.py # IACR ePrint scraper +│ └── search-paper/ # Unified academic search + venue resolution +│ ├── SKILL.md # Orchestrates the layered venue lookup +│ ├── arxiv.py # arXiv API +│ ├── iacr.py # IACR ePrint scraper +│ ├── semantic_scholar.py # Citations + venue lookup (by arXiv ID or title) +│ ├── dblp.py # CS-authoritative venue lookup (by title) +│ └── openalex.py # Broad-coverage venue lookup (by title) ├── tests/ # Python tests ├── dev/ │ ├── ROADMAP.md # This file @@ -271,24 +272,29 @@ And each skill works standalone: invoke `analyze-paper paper.pdf` for just a str **Methodology stage:** Enriches Stage 1b (establish baseline from literature) with real academic paper servers. -**Goal:** Upgrade `/review-literature` from generic web search to structured academic search — arXiv, IACR ePrint, citation graph traversal — using lightweight Python scripts (no MCP dependency). Also enable `/investigate` to pull in new references mid-loop when a cycle reveals a gap in context. +**Goal:** Upgrade `/review-literature` from generic web search to structured academic search — a unified `/search-paper` skill that fans out over arXiv, IACR ePrint, Semantic Scholar, DBLP, and OpenAlex for paper search, citation graph traversal, and publication-venue resolution — using lightweight Python scripts (no MCP dependency). Also enable `/investigate` to pull in new references mid-loop when a cycle reveals a gap in context. -**What success looks like:** invoking the `/review-literature` skill with `"post-quantum threshold signatures"` automatically searches arXiv and IACR ePrint, traces forward/backward citations via Semantic Scholar, and produces a structured literature survey with precise references. +**What success looks like:** invoking the `/review-literature` skill with `"post-quantum threshold signatures"` delegates to `/search-paper` for paper search, citation graph traversal, and layered venue resolution, and produces a structured literature survey with real publication venues (e.g. CRYPTO 2023) in the references. #### Search Tools | Script | Location | Capabilities | Dependencies | |--------|----------|--------------|-------------| -| `search_arxiv.py` | `skills/search-arxiv/` | `search`, `download`, `citations` (via Semantic Scholar) | `pip install arxiv requests` | -| `search_iacr.py` | `skills/search-iacr/` | `search`, `recent`, `download`, `url` | `pip install requests beautifulsoup4` | +| `arxiv.py` | `skills/search-paper/` | `search`, `recent`, `download`, `journal-ref` | `pip install arxiv` | +| `iacr.py` | `skills/search-paper/` | `search`, `recent`, `download`, `url`, `pubinfo` | `pip install requests beautifulsoup4` | +| `semantic_scholar.py` | `skills/search-paper/` | `venue` (by arXiv ID or title), `citations` | `pip install requests` | +| `dblp.py` | `skills/search-paper/` | `venue` (by title + author) | `pip install requests` | +| `openalex.py` | `skills/search-paper/` | `venue` (by title) | `pip install requests` | + +The `/search-paper` `SKILL.md` orchestrates a layered venue resolver: Semantic Scholar → archive's own field (arXiv `journal_ref` / ePrint `Publication info`) → DBLP → OpenAlex → `(preprint)` label. #### Tasks -- [x] Build `/search-arxiv` skill with Python script (arXiv API + Semantic Scholar citations) -- [x] Build `/search-iacr` skill with Python script (IACR ePrint scraper) -- [x] Write `references/search-tools.md` — catalog of search tools with usage patterns and decision tree -- [x] Update `/review-literature` skill: structured search as primary, WebSearch as fallback, citation graph, recent papers +- [x] Build `/search-paper` skill bundling arXiv + IACR + Semantic Scholar + DBLP + OpenAlex drivers +- [x] Write `references/search-tools.md` — catalog of search tools with usage patterns, decision tree, and venue-resolution protocol +- [x] Update `/review-literature` skill: structured search as primary, WebSearch as fallback, citation graph, recent papers, layered venue resolution per kept paper - [x] Update `/investigate` skill: mid-cycle literature search via search scripts +- [x] Update `/synthesize` skill: References section uses resolved venues, never raw archive IDs - [x] Handle graceful degradation when search scripts are unavailable - [x] Document Python prerequisites in README - [ ] Test: given a seed paper, can Reaper find and summarize the 10 most relevant related works? @@ -415,7 +421,7 @@ Different models have different strengths. The critique skill should route consu | **Cross-agent installer** | `npx skills add SebastianElvis/reaper` | ✓ | | **Pin syntax** | `npx skills add SebastianElvis/reaper#v0.3.9` (git tags) | ✓ | | **Inter-skill calls** | Host-agnostic prose ("invoke the `` skill") | ✓ | -| **Python script bundling** | Whole-directory copy includes `search_arxiv.py`, `search_iacr.py`, `references/` | ✓ | +| **Python script bundling** | Whole-directory copy includes `arxiv.py`, `iacr.py`, `semantic_scholar.py`, `dblp.py`, `openalex.py`, `references/` | ✓ | | **Frontmatter compatibility** | Claude-only keys (`user-invocable`, `argument-hint`, hooks) preserved as opaque YAML, no-op on other hosts | ✓ | | **CI validation** | Frontmatter regex check + strict `npx skills add` discovery test (verifies every expected skill, Python script, and reference file is present after install; fails the build if any asset is missing) | ✓ | | **Claude Code plugin path** | `.claude-plugin/marketplace.json` for slash-command routing | ✓ | diff --git a/evals/evals.json b/evals/evals.json index f5052ca..93731f0 100644 --- a/evals/evals.json +++ b/evals/evals.json @@ -106,12 +106,16 @@ "test": "report.md has a one-sentence central finding, bulleted refutable contributions, each finding starts with a concrete example, no chronological narration, open questions are specific" }, { - "skill": "search-arxiv", - "test": "search command returns valid JSON array with fields: arxiv_id, title, authors, year, abstract, pdf_url. citations command returns references and citations arrays. Tested via: python -m pytest tests/test_search_arxiv.py" + "skill": "search-paper (arxiv.py)", + "test": "search command returns valid JSON array with fields: arxiv_id, title, authors, year, abstract, pdf_url, journal_ref. journal-ref command returns the author-supplied venue field. Tested via: python -m pytest tests/test_search_paper.py" }, { - "skill": "search-iacr", - "test": "search command returns valid JSON array with fields: eprint_id, title, pdf_url, url. url command returns correct ePrint URLs. Tested via: python -m pytest tests/test_search_iacr.py" + "skill": "search-paper (iacr.py)", + "test": "search command returns valid JSON array with fields: eprint_id, title, pdf_url, url. url and pubinfo commands return correct ePrint URLs and 'Publication info' text. Tested via: python -m pytest tests/test_search_paper.py" + }, + { + "skill": "search-paper (venue resolver)", + "test": "semantic_scholar.py / dblp.py / openalex.py venue commands return JSON with `found`, `venue`, `venue_full`, `year` fields. Layered protocol in SKILL.md walks them in order and stops at first success. Tested via: python -m pytest tests/test_search_paper.py" }, { "skill": "review-literature (H2)", diff --git a/skills/investigate/SKILL.md b/skills/investigate/SKILL.md index 5d6a0c8..27e1c70 100644 --- a/skills/investigate/SKILL.md +++ b/skills/investigate/SKILL.md @@ -272,7 +272,7 @@ Run all N cycles. The only valid early stop is **genuine convergence**: all hypo ## When Stuck -If a cycle is going nowhere, follow the escalation protocol in `{{REAPER_SKILL_DIR}}/references/methodology.md` (section "When Stuck: 8-Step Escalation"). The steps progress from re-reading existing materials, through searching for new literature (see `{{REAPER_SKILL_DIR}}/references/search-tools.md` for search commands, which use `search_arxiv.py` and `search_iacr.py`), to trying radically different approaches. +If a cycle is going nowhere, follow the escalation protocol in `{{REAPER_SKILL_DIR}}/references/methodology.md` (section "When Stuck: 8-Step Escalation"). The steps progress from re-reading existing materials, through searching for new literature (see `{{REAPER_SKILL_DIR}}/references/search-tools.md` for search commands, which use the `arxiv.py` and `iacr.py` scripts in the `/search-paper` skill), to trying radically different approaches. When searching for new literature mid-investigation, download relevant papers to `reaper-workspace/papers/`, write per-paper notes (`-notes.md`), and **integrate findings into `reaper-workspace/notes/literature.md` inline** — add new entries to the appropriate existing sections rather than appending a separate "Mid-Investigation Additions" section. Log the search as a cycle with action-type `literature-search` in `notes/results.md`. diff --git a/skills/reaper/SKILL.md b/skills/reaper/SKILL.md index 8f97e3a..b343af3 100644 --- a/skills/reaper/SKILL.md +++ b/skills/reaper/SKILL.md @@ -30,7 +30,7 @@ When `--codex` is set, propagate this context to all skill invocations. Each ski ## Peer Skills -This orchestrator chains 8 sub-skills that must be installed alongside it: `/clarify-goal`, `/analyze-paper`, `/review-literature`, `/formalize-problem`, `/brainstorm`, `/investigate`, `/critique`, `/synthesize`. Two more (`/search-arxiv`, `/search-iacr`) are called transitively by `/review-literature` and `/investigate`. The `/` form is the canonical display convention used in these docs; substitute the host-native invocation form (slash command, auto-discovery, manual `SKILL.md` pointer) when actually running them. +This orchestrator chains 8 sub-skills that must be installed alongside it: `/clarify-goal`, `/analyze-paper`, `/review-literature`, `/formalize-problem`, `/brainstorm`, `/investigate`, `/critique`, `/synthesize`. One more (`/search-paper`) is called transitively by `/review-literature` and `/investigate`. The `/` form is the canonical display convention used in these docs; substitute the host-native invocation form (slash command, auto-discovery, manual `SKILL.md` pointer) when actually running them. If any of these are missing from your agent's skills folder, ask the user to reinstall the full Reaper package (`npx skills add SebastianElvis/reaper`). @@ -72,9 +72,9 @@ reaper-workspace/ | Category | Files | Policy | |----------|-------|--------| -| Evolving (inline edit) | `notes/current-understanding.md`, `notes/results.md`, `notes/ideas.md`, `notes/problem-statement.md`, `papers/*-notes.md`, `investigations/*/analysis.md`, `investigations/*/proof.md` | Edit in place. Single writer per file per batch. | +| Evolving (inline edit) | `notes/current-understanding.md`, `notes/results.md`, `notes/ideas.md`, `notes/problem-statement.md`, `notes/literature.md`, `papers/*-notes.md`, `investigations/*/analysis.md`, `investigations/*/proof.md` | Edit in place. Single writer per file per batch. `notes/literature.md` is created by `/review-literature` and extended inline by `/investigate` during mid-cycle literature search. | | Append-only | `logs/cycle-*.md`, `feedbacks/round-*.md`, `feedbacks/codex-consultation-*.md` | Create once, never modify. | -| Write-once | `notes/paper-summary.md`, `notes/literature.md`, `notes/clarified-goal.md`, `report.md` | Created by one skill. May be regenerated on re-run but not incrementally edited by other skills. | +| Write-once | `notes/paper-summary.md`, `notes/clarified-goal.md`, `report.md` | Created by one skill. May be regenerated on re-run but not incrementally edited by other skills. | **File naming conventions:** Investigation dirs: `NNN-/` (zero-padded). Cycle logs: `cycle-NNN-.md`. Feedback: `round-N.md`, `codex-consultation-N.md`. Paper notes: `-notes.md`. @@ -215,8 +215,7 @@ Do **not** block waiting for a response — the pipeline is complete. The user c | `/investigate` | `problem-statement.md`, `ideas.md`, `current-understanding.md`, `results.md` | Updates `results.md`, `current-understanding.md`, `ideas.md`; creates `investigations/*`, `logs/*` | | `/critique` | `current-understanding.md`, `results.md`, `problem-statement.md`, `ideas.md` | `feedbacks/*`; may update `ideas.md` | | `/synthesize` | `current-understanding.md`, `results.md`, `problem-statement.md`, `ideas.md` | `report.md` | -| `/search-arxiv` | (query) | stdout | -| `/search-iacr` | (query) | stdout | +| `/search-paper` | (query) | stdout — search results, citations, or resolved venue | ## Important Notes diff --git a/skills/reaper/references/search-tools.md b/skills/reaper/references/search-tools.md index 8b5b9e5..c376669 100644 --- a/skills/reaper/references/search-tools.md +++ b/skills/reaper/references/search-tools.md @@ -1,36 +1,37 @@ # Search Tools Reference -Reaper uses Python scripts to search academic paper archives. This document catalogs the available tools, when to use each, and common workflow patterns. +Reaper uses the `/search-paper` skill for all academic paper search, citation graph traversal, and publication-venue resolution. The skill ships five Python scripts under one directory; this document catalogs them and shows the common workflows. ## Path Resolution Protocol -The scripts referenced below live in sibling skills (`search-arxiv/` and `search-iacr/`). The placeholders **`{{SEARCH_ARXIV_SKILL_DIR}}`** and **`{{SEARCH_IACR_SKILL_DIR}}`** below are template tokens — **you MUST substitute each with the absolute install path of the corresponding sibling skill before invoking, or the exec will fail.** Common install locations (substitute the trailing skill name as needed): +The scripts referenced below live in the sibling `/search-paper` skill. The placeholder **`{{SEARCH_PAPER_SKILL_DIR}}`** below is a template token — **you MUST substitute it with the absolute install path of the sibling skill before invoking, or the exec will fail.** Common install locations: -- `~/.claude/skills//` (Claude Code) -- `~/.cursor/skills//` (Cursor) -- `~/.agents/skills//` (Codex CLI, Cline, Gemini CLI, Copilot, OpenCode, Warp, Goose, Replit — universal target) -- `~/.continue/skills//` (Continue) -- `~/.windsurf/skills//` (Windsurf) -- `/skills//` (during repo development) +- `~/.claude/skills/search-paper/` (Claude Code) +- `~/.cursor/skills/search-paper/` (Cursor) +- `~/.agents/skills/search-paper/` (Codex CLI, Cline, Gemini CLI, Copilot, OpenCode, Warp, Goose, Replit — universal target) +- `~/.continue/skills/search-paper/` (Continue) +- `~/.windsurf/skills/search-paper/` (Windsurf) +- `/skills/search-paper/` (during repo development) -**Sibling-skill dependency**: This reference assumes the full `/reaper` package was installed together (`npx skills add SebastianElvis/reaper`) so that `reaper/`, `search-arxiv/`, and `search-iacr/` are co-located in your agent's skills folder. +**Sibling-skill dependency**: This reference assumes the full `/reaper` package was installed together (`npx skills add SebastianElvis/reaper`) so that `reaper/` and `search-paper/` are co-located in your agent's skills folder. -## Tools +## Scripts -### search_arxiv.py +### `arxiv.py` — arXiv preprint server -**Location**: `{{SEARCH_ARXIV_SKILL_DIR}}/search_arxiv.py` -**Dependencies**: `pip install arxiv requests` +**Location**: `{{SEARCH_PAPER_SKILL_DIR}}/arxiv.py` +**Dependencies**: `pip install arxiv` | Command | Purpose | Key Parameters | |---------|---------|---------------| -| `search ""` | Find papers by topic | `--max-results N`, `--categories cs.CR,cs.DC` | +| `search ""` | Find papers by topic (sorted by relevance) | `--max-results N`, `--categories cs.CR,cs.DC` | +| `recent [""]` | Get recently submitted papers (sorted by date) | `--max-results N`, `--categories cs.CR` | | `download ` | Download paper PDF | `--output-dir DIR` | -| `citations ` | Forward + backward citations via Semantic Scholar | `--max-results N` | +| `journal-ref ` | Read author-supplied venue field | — | -**When to use**: Broad CS topics (distributed systems, complexity theory, algorithms), papers that appear on arXiv. Also use `citations` for any paper with an arXiv ID regardless of primary venue. +**When to use**: Broad CS topics (distributed systems, complexity theory, algorithms), papers that appear on arXiv. -**Output**: JSON to stdout. Search returns array of `{arxiv_id, title, authors, year, abstract, categories, pdf_url, published}`. Citations returns `{references: [...], citations: [...]}`. +**Output**: JSON to stdout. Search returns array of `{arxiv_id, title, authors, year, abstract, categories, pdf_url, published, journal_ref}`. **Categories for crypto/distributed systems**: - `cs.CR` — Cryptography and Security @@ -38,9 +39,9 @@ The scripts referenced below live in sibling skills (`search-arxiv/` and `search - `cs.DS` — Data Structures and Algorithms - `cs.CC` — Computational Complexity -### search_iacr.py +### `iacr.py` — IACR ePrint archive -**Location**: `{{SEARCH_IACR_SKILL_DIR}}/search_iacr.py` +**Location**: `{{SEARCH_PAPER_SKILL_DIR}}/iacr.py` **Dependencies**: `pip install requests beautifulsoup4` | Command | Purpose | Key Parameters | @@ -49,15 +50,53 @@ The scripts referenced below live in sibling skills (`search-arxiv/` and `search | `recent` | Get latest ePrint papers | `--max-results N` | | `download ` | Download paper PDF | `--output-dir DIR` | | `url ` | Get paper URL | — | +| `pubinfo ` | Read "Publication info" line (venue) | — | -**When to use**: Cryptography and security topics. IACR ePrint is the primary preprint server for cryptography — use this for all crypto-related searches. Top 5 search results are automatically enriched with metadata (title, authors, abstract) from the paper page. +**When to use**: Cryptography and security topics. IACR ePrint is the primary preprint server for cryptography. Top 5 search results are automatically enriched with metadata (title, authors, abstract, publication_info) from the paper page. -**Output**: JSON to stdout. Search returns array of `{eprint_id, title, authors, year, abstract, pdf_url, url}`. ePrint IDs are in `YYYY/NNNN` format. +**Output**: JSON to stdout. Search returns array of `{eprint_id, title, authors, year, abstract, publication_info, venue, pdf_url, url}`. ePrint IDs are in `YYYY/NNNN` format. + +### `semantic_scholar.py` — Semantic Scholar metadata + +**Location**: `{{SEARCH_PAPER_SKILL_DIR}}/semantic_scholar.py` +**Dependencies**: `pip install requests` + +| Command | Purpose | Key Parameters | +|---------|---------|---------------| +| `venue --arxiv ` | Look up venue by arXiv ID | — | +| `venue --title ""` | Look up venue by title | `--author "<surname>"` | +| `citations <arxiv_id>` | Forward + backward citations | `--max-results N` | + +**When to use**: Citation graph traversal (any paper with an arXiv ID). Primary venue resolver — Semantic Scholar covers the most papers. + +**Output**: JSON to stdout. `venue` returns `{found, venue, venue_full, venue_type, year, title, authors}`. `citations` returns `{arxiv_id, references, citations}` with each entry including `venue` when known. + +### `dblp.py` — DBLP metadata + +**Location**: `{{SEARCH_PAPER_SKILL_DIR}}/dblp.py` +**Dependencies**: `pip install requests` + +| Command | Purpose | Key Parameters | +|---------|---------|---------------| +| `venue "<title>"` | Look up venue by title | `--author "<surname>"` | + +**When to use**: Authoritative for CS conference and journal venues. Use as the title-based fallback when Semantic Scholar fails. + +### `openalex.py` — OpenAlex metadata + +**Location**: `{{SEARCH_PAPER_SKILL_DIR}}/openalex.py` +**Dependencies**: `pip install requests` + +| Command | Purpose | Key Parameters | +|---------|---------|---------------| +| `venue "<title>"` | Look up venue by title | — | + +**When to use**: Broad coverage beyond CS (non-CS journals, niche workshops, books). Final fallback before labeling a paper as preprint-only. ### WebSearch (built-in) **When to use as fallback**: -- Conference proceedings not on arXiv/ePrint (PODC, DISC, STOC, FOCS proceedings) +- Conference proceedings not on arXiv/ePrint (some PODC/DISC/STOC/FOCS proceedings) - Blog posts, talks, informal write-ups - Author homepages with preprints - When Python scripts fail (missing deps, network issues) @@ -76,51 +115,63 @@ Is the topic cryptography/security? Always supplement with WebSearch for non-academic sources. Need citation context? -├── Have arXiv ID → use search_arxiv.py citations +├── Have arXiv ID → use semantic_scholar.py citations └── No arXiv ID → use WebSearch for "cited by" / "references" Need very recent papers? -└── Use search_iacr.py recent + arXiv search sorted by date +└── Use iacr.py recent + arxiv.py recent (both sort by submission date) + +Need to resolve a paper's publication venue? +└── Follow the layered Venue Resolution Protocol below. ``` +## Venue Resolution Protocol + +Every paper that appears in `notes/literature.md` or the `## References` section of `report.md` must have a real publication venue (CRYPTO, S&P, PODC, …) — not just an archive ID. + +The authoritative layered protocol lives in the `/search-paper` skill's own `SKILL.md` ("Venue Resolution Protocol" section). At a glance, it walks Semantic Scholar → author-supplied field (arXiv `journal-ref` / ePrint `pubinfo`) → DBLP → OpenAlex, stopping at the first success, and labels the paper `(preprint)` if all layers fail. Callers should invoke `/search-paper` rather than orchestrating the layers themselves, and cache the resolved venue in their workspace notes to avoid re-resolving across cycles. + ## Common Workflow Patterns ### Full Literature Review 1. **Parallel search**: Spawn 3 subagents - - Subagent 1: `search_arxiv.py search` with 3-4 diverse queries - - Subagent 2: `search_iacr.py search` with 3-4 diverse queries + - Subagent 1: `arxiv.py search` with 3-4 diverse queries + - Subagent 2: `iacr.py search` with 3-4 diverse queries - Subagent 3: WebSearch for non-academic sources 2. **Merge and deduplicate** results across all sources -3. **Citation graph**: For seed paper + top 3 results, run `search_arxiv.py citations` -4. **Recent check**: `search_iacr.py recent` to catch very new papers +3. **Citation graph**: For seed paper + top 3 results, run `semantic_scholar.py citations` +4. **Recent check**: `iacr.py recent` to catch very new papers 5. **Filter and prioritize** by relevance +6. **Resolve venue** for every kept paper using the layered protocol above ### Mid-Investigation Literature Search 1. Run a focused query on the specific question that arose: ```bash - python {{SEARCH_IACR_SKILL_DIR}}/search_iacr.py search "exact technical question" --max-results 5 - python {{SEARCH_ARXIV_SKILL_DIR}}/search_arxiv.py search "exact technical question" --max-results 5 + python {{SEARCH_PAPER_SKILL_DIR}}/iacr.py search "exact technical question" --max-results 5 + python {{SEARCH_PAPER_SKILL_DIR}}/arxiv.py search "exact technical question" --max-results 5 ``` 2. If a highly relevant paper is found, download and read it: ```bash - python {{SEARCH_ARXIV_SKILL_DIR}}/search_arxiv.py download <id> --output-dir reaper-workspace/papers/ + python {{SEARCH_PAPER_SKILL_DIR}}/arxiv.py download <id> --output-dir reaper-workspace/papers/ ``` -3. Integrate findings into `literature.md` inline (add to appropriate existing sections) +3. Resolve its venue via the layered protocol; integrate findings into `literature.md` inline (add to appropriate existing sections) ### Citation Chasing 1. Start with a known paper's arXiv ID 2. Get references (backward) and citations (forward): ```bash - python {{SEARCH_ARXIV_SKILL_DIR}}/search_arxiv.py citations 2305.12345 --max-results 20 + python {{SEARCH_PAPER_SKILL_DIR}}/semantic_scholar.py citations 2305.12345 --max-results 20 ``` 3. For each highly relevant citation, recursively chase (1-2 hops max) ## Rate Limits and Reliability - **arXiv API**: No authentication needed. Recommended: ≤1 request/second. The `arxiv` Python package handles rate limiting. -- **IACR ePrint**: No documented rate limits. Scrapes HTML search results, so may break if the site redesigns. Top 5 results fetch individual paper pages (5 additional requests). -- **Semantic Scholar** (used by `citations`): Free tier allows 100 requests/5 minutes without API key. Sufficient for citation graph traversal. +- **IACR ePrint**: No documented rate limits. Scrapes HTML, may break if the site redesigns. Top 5 search results fetch individual paper pages (5 additional requests per search). +- **Semantic Scholar**: Free tier allows 100 requests / 5 minutes without API key. Sufficient for citation graph traversal and the bulk of venue lookups. +- **DBLP**: No documented hard limits, but be polite — at most a few requests per second. +- **OpenAlex**: Free, no auth needed. Consider passing `mailto=<your-email>` as a parameter to enter the polite pool with higher limits. - **Graceful degradation**: All skills that use these tools must fall back to WebSearch if scripts fail. diff --git a/skills/review-literature/SKILL.md b/skills/review-literature/SKILL.md index 1eb0632..c767961 100644 --- a/skills/review-literature/SKILL.md +++ b/skills/review-literature/SKILL.md @@ -17,19 +17,6 @@ Invoke this skill by name with the research topic as a quoted string. On slash-c review-literature "post-quantum threshold signatures" ``` -## Path Resolution Protocol - -This skill references files and scripts in sibling skills. The placeholders **`{{REAPER_SKILL_DIR}}`**, **`{{SEARCH_ARXIV_SKILL_DIR}}`**, and **`{{SEARCH_IACR_SKILL_DIR}}`** below are template tokens — **you MUST substitute each with the absolute install path of the corresponding sibling skill before reading or invoking, or the read/exec will fail.** Common install locations (substitute the trailing skill name as needed — `reaper`, `search-arxiv`, `search-iacr`): - -- `~/.claude/skills/<skill>/` (Claude Code) -- `~/.cursor/skills/<skill>/` (Cursor) -- `~/.agents/skills/<skill>/` (Codex CLI, Cline, Gemini CLI, Copilot, OpenCode, Warp, Goose, Replit — universal target) -- `~/.continue/skills/<skill>/` (Continue) -- `~/.windsurf/skills/<skill>/` (Windsurf) -- `<repo-root>/skills/<skill>/` (during repo development) - -**Sibling-skill dependency**: This skill assumes the full `/reaper` package was installed together (`npx skills add SebastianElvis/reaper`) so that `reaper/`, `search-arxiv/`, and `search-iacr/` are co-located in your agent's skills folder. Single-skill installs will fail to resolve sibling references. - ## Instructions ### 1. Gather Context @@ -44,21 +31,9 @@ Combine with the research goal to formulate search queries. ### 2. Search — Structured Sources (Primary) -Use the search scripts via Bash to query arXiv and IACR ePrint. Generate multiple diverse queries per source. (The placeholders `{{SEARCH_ARXIV_SKILL_DIR}}` and `{{SEARCH_IACR_SKILL_DIR}}` below are defined in the Path Resolution Protocol section above — substitute the absolute install paths before invoking. Alternatively, invoke the `/search-arxiv` and `/search-iacr` skills by name through your host's skill mechanism.) - -**arXiv** (broad CS/math — use for distributed systems, complexity, general crypto): +Delegate all paper search to the `/search-paper` skill. It decides which archives to hit (arXiv, IACR ePrint, …), which categories or filters apply for the topic, and returns structured results the caller can merge. The caller's job is to supply good queries, not pick archives. -```bash -python {{SEARCH_ARXIV_SKILL_DIR}}/search_arxiv.py search "<query>" --max-results 10 --categories cs.CR,cs.DC -``` - -**IACR ePrint** (cryptography-specific — use for all crypto topics): - -```bash -python {{SEARCH_IACR_SKILL_DIR}}/search_iacr.py search "<query>" --max-results 10 -``` - -**Query types** (generate at least one query per type, per source): +**Query types** (generate at least one query per type): - **Direct**: the exact problem (e.g., "BFT consensus communication complexity lower bound") - **Author-based**: key authors in the area (e.g., "Yin Malkhi Abraham consensus") @@ -67,10 +42,7 @@ python {{SEARCH_IACR_SKILL_DIR}}/search_iacr.py search "<query>" --max-results 1 - **Attacks/impossibilities**: known negative results (e.g., "FLP impossibility", "DLS lower bound") - **Surveys**: SoK papers, systematization of knowledge (e.g., "SoK blockchain consensus") -**Spawn parallel subagents** (using your host's parallel-spawn primitive — e.g. Claude Code's `Agent` tool — or run sequentially if unavailable) for concurrent search: -- **Subagent 1**: arXiv searches (multiple queries with different categories) -- **Subagent 2**: IACR ePrint searches (multiple queries) -- **Subagent 3**: WebSearch fallback (see step 3) +**Spawn parallel subagents** (using your host's parallel-spawn primitive — e.g. Claude Code's `Agent` tool — or run sequentially if unavailable) for concurrent search — one subagent per query type, each invoking `/search-paper`. Run the WebSearch fallback (step 3) as an additional parallel subagent. **Context efficiency**: Do NOT pass the full `paper-summary.md` to each search subagent. Instead, extract the key search terms (topic, author names, 3-5 key concepts) and pass those as a brief JSON object (~100 words). Each subagent only needs the terms to formulate queries, not the full paper analysis. @@ -87,11 +59,7 @@ This runs as a parallel subagent alongside the structured searches. ### 4. Citation Graph Traversal -For the **seed paper** (from `paper-summary.md`) and the **top 3 most relevant results**, trace citations: - -```bash -python {{SEARCH_ARXIV_SKILL_DIR}}/search_arxiv.py citations <arxiv_id> --max-results 20 -``` +For the **seed paper** (from `paper-summary.md`) and the **top 3 most relevant results**, invoke the `/search-paper` skill to traverse the citation graph (pass the arXiv ID and request up to 20 citations in each direction — forward and backward). This returns both: - **References** (backward): what this paper builds on — find foundational results @@ -103,13 +71,7 @@ Deduplicate results across all search sources. ### 5. Recent Papers Check -For fast-moving areas, check for very recent publications: - -```bash -python {{SEARCH_IACR_SKILL_DIR}}/search_iacr.py recent --max-results 10 -``` - -Scan titles/abstracts for relevance to the research goal. Include any relevant recent papers that the main search may have missed. +For fast-moving areas, invoke the `/search-paper` skill to pull the most-recently-posted papers in the area (e.g. top 10). Scan titles/abstracts for relevance to the research goal and include any relevant recent papers that the main search may have missed. ### 6. Filter and Prioritize @@ -120,7 +82,7 @@ For each result found, assess relevance to the research goal. Classify each pape #### Venue and Author Weighting -Weight results heavily toward top venues. A peer-reviewed top-conference paper is far more trustworthy than an unreviewed preprint. Consult `{{REAPER_SKILL_DIR}}/references/venue-tiers.md` (placeholder defined in the Path Resolution Protocol section above) for the domain-appropriate venue tier table and author weighting criteria. +Weight results heavily toward top venues. A peer-reviewed top-conference paper is far more trustworthy than an unreviewed preprint. Consult the `/reaper` skill's `references/venue-tiers.md` for the domain-appropriate venue tier table and author weighting criteria. When two papers make competing claims, prefer the one from the higher-tier venue by authors with more domain-specific expertise. When a preprint contradicts a published top-venue result, flag it but do not treat the preprint as authoritative without independent verification. @@ -133,17 +95,17 @@ Within each category (same-goal / same-approach), assess relevance: Keep high and medium relevance results. Discard low unless it's a seminal work or by a tier-1 venue / leading author in the area. -### 7. Download and Analyze Key Papers +### 7. Resolve Publication Venues -For all **high-relevance** papers (and medium-relevance papers that seem particularly important), download the PDF to the local workspace: +For every kept paper, resolve the actual publication venue (CRYPTO, S&P, PODC, …) — an arXiv or ePrint ID is not a venue. **Invoke the `/search-paper` skill's Venue Resolution Protocol** with the best identifier available — arXiv ID preferred, else ePrint ID, else title plus first-author surname. `/search-paper` walks the layered Semantic Scholar → author-supplied field → DBLP → OpenAlex ladder and returns either a resolved venue or the explicit `(preprint)` label. -```bash -# arXiv papers -python {{SEARCH_ARXIV_SKILL_DIR}}/search_arxiv.py download <arxiv_id> --output-dir reaper-workspace/papers +Record the resolved venue inline in your scratch notes so downstream steps and re-runs don't repeat the work. Never guess a venue from topic or affiliation — if `/search-paper` returns `(preprint)`, use that label verbatim. -# IACR ePrint papers -python {{SEARCH_IACR_SKILL_DIR}}/search_iacr.py download <eprint_id> --output-dir reaper-workspace/papers -``` +**Spawn parallel subagents** (one per kept paper, using your host's parallel-spawn primitive) to resolve venues concurrently — each lookup is independent. + +### 8. Download and Analyze Key Papers + +For all **high-relevance** papers (and medium-relevance papers that seem particularly important), invoke the `/search-paper` skill to download each PDF into `reaper-workspace/papers/` (pass the arXiv ID or ePrint ID). After downloading, **delegate paper reading to `/analyze-paper`**. For each downloaded paper, invoke the `/analyze-paper` skill with: @@ -159,7 +121,7 @@ The `/analyze-paper` skill handles the multi-pass reading (calibrating depth by These notes serve as a durable reference for the investigate step. They are evolving files — update inline if revisited during mid-investigation search. -### 8. Cross-Reference Verification +### 9. Cross-Reference Verification Using the per-paper notes produced by `/analyze-paper` in the previous step, check whether the paper under analysis correctly cites and uses each high-relevance work: @@ -170,7 +132,7 @@ Using the per-paper notes produced by `/analyze-paper` in the previous step, che Document any discrepancies in the per-paper notes (`<id>-notes.md`) under a `### Discrepancies with Paper Under Analysis` heading. Summarize all discrepancies in the `## Gaps Identified` section of the output file — these are high-priority inputs for the formalize-problem step. -### 9. Write Output +### 10. Write Output Write to `reaper-workspace/notes/literature.md`: @@ -187,7 +149,9 @@ Papers that address the same or a closely related research goal. | # | Title | Authors | Year | Venue | Key Contribution | Relation to Our Goal | Link | Local Path | |---|-------|---------|------|-------|-----------------|---------------------|------|------------| -| 1 | ... | ... | ... | ... | [1-sentence] | [how this relates to our specific goal] | [arXiv](https://arxiv.org/abs/XXXX.XXXXX) or [ePrint](https://eprint.iacr.org/YYYY/NNNN) | `papers/<filename>` | +| 1 | ... | ... | ... | CRYPTO 2023 *or* `(preprint)` | [1-sentence] | [how this relates to our specific goal] | [arXiv](https://arxiv.org/abs/XXXX.XXXXX) or [ePrint](https://eprint.iacr.org/YYYY/NNNN) | `papers/<filename>` | + +The `Venue` column holds the resolved publication venue from Step 7 — never the archive ID. Use `(preprint)` only when `/search-paper` returned no venue. ## Same-Approach Works @@ -195,7 +159,7 @@ Papers that apply similar techniques or approaches to different problems. | # | Title | Authors | Year | Venue | Key Contribution | Shared Technique | Link | Local Path | |---|-------|---------|------|-------|-----------------|-----------------|------|------------| -| 1 | ... | ... | ... | ... | [1-sentence] | [what technique/approach we share and how they use it] | [arXiv](https://arxiv.org/abs/XXXX.XXXXX) or [ePrint](https://eprint.iacr.org/YYYY/NNNN) | `papers/<filename>` | +| 1 | ... | ... | ... | CRYPTO 2023 *or* `(preprint)` | [1-sentence] | [what technique/approach we share and how they use it] | [arXiv](https://arxiv.org/abs/XXXX.XXXXX) or [ePrint](https://eprint.iacr.org/YYYY/NNNN) | `papers/<filename>` | ## Citation Graph @@ -220,21 +184,28 @@ Summary of all downloaded papers and their local paths for quick reference durin ### Graceful Degradation -If the Python search scripts fail (missing dependencies, network errors): -1. Fall back to WebSearch for all queries. -2. Note in `literature.md` that structured search was unavailable, with a timestamp and error message: `> **Note**: arXiv/ePrint API search was unavailable for this review (error: ...). Results are from web search only.` +`/search-paper` can fail at any step independently (search, citation graph, venue resolution, download). Apply the right fallback per step: -If PDF download fails for a paper, note it in the table (leave Local Path as "unavailable") and proceed with abstract-level understanding. The review must still meet quality criteria even in degraded mode. +- **Search fails entirely** (all queries error out): fall back to WebSearch for all queries and add a top-of-file note to `literature.md`: `> **Note**: structured paper search via /search-paper was unavailable for this review (error: ...). Results are from web search only.` +- **Citation graph fails** (step 4): skip the Citation Graph section or populate it from WebSearch; annotate each affected paper's row: "citation graph unavailable". +- **Venue resolution fails** for a specific paper (step 7): label that paper `(preprint)` in the `Venue` column rather than blocking the review. +- **Download fails** for a specific paper (step 8): leave `Local Path` as "unavailable" and proceed with abstract-level understanding for that paper. + +In every case, the review still proceeds. Only the quality criteria that depend on the failed step are relaxed (see Quality Criteria below). ### Quality Criteria +**Content criteria (always required):** - At least 10 relevant works found (unless the area is very narrow) -- Results include papers from both arXiv and IACR ePrint (when the topic is crypto-related) - Papers are split into same-goal and same-approach categories — both categories should have entries -- High-relevance papers are downloaded and analyzed via `analyze-paper --goal`, with per-paper notes in `reaper-workspace/papers/` +- High-relevance papers are downloaded (when `/search-paper` download succeeds) and analyzed via `analyze-paper --goal`, with per-paper notes in `reaper-workspace/papers/` - Per-paper notes (produced by `/analyze-paper`) contain structured analysis with key results, strengths/weaknesses, and relevance assessment — not just abstract-level summaries -- Citation graph section shows forward and backward citations for key papers +- **Every entry in the Venue column is a real publication venue (e.g., CRYPTO 2023, S&P 2024) or the explicit `(preprint)` label** — never just an arXiv/ePrint ID - Landscape summary gives a reader unfamiliar with the area a useful mental map - Each related work has a specific relevance statement (not just "related to our topic") - Gaps section identifies concrete unexplored directions, not vague "more work needed" - No hallucinated papers — every entry must come from a real search result + +**Normal-mode criteria (required unless explicitly degraded):** +- Results draw from multiple structured sources via `/search-paper` (not WebSearch alone) — waived when the `/search-paper` availability note is present +- Citation graph section shows forward and backward citations for key papers — waived when step 4 is marked unavailable diff --git a/skills/search-arxiv/SKILL.md b/skills/search-arxiv/SKILL.md deleted file mode 100644 index 9bb1471..0000000 --- a/skills/search-arxiv/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: search-arxiv -description: "Search arXiv for academic papers, download PDFs, and retrieve citation graphs. Use when you need to find papers on arXiv by topic, download a specific paper, or trace forward/backward citations." -user-invocable: true -argument-hint: "<query> [--max-results N] [--categories cs.CR,cs.DC]" ---- - -# Search arXiv - -Search arXiv for academic papers using the `arxiv` Python package, with citation graph support via Semantic Scholar. - -## Usage - -Invoke this skill by name with the query as a quoted string. On slash-command hosts, prefix with `/` (e.g. `/search-arxiv "<query>"`). - -``` -search-arxiv "post-quantum threshold signatures" --max-results 15 --categories cs.CR -``` - -## Path Resolution Protocol - -This skill wraps `search_arxiv.py`, which lives in the **same directory as this `SKILL.md`**. **`{{SKILL_DIR}}`** below is a template placeholder — **you MUST substitute it with the absolute install path of this skill before invoking, or the exec will fail.** Common install locations: - -- `~/.claude/skills/search-arxiv/` (Claude Code) -- `~/.cursor/skills/search-arxiv/` (Cursor) -- `~/.agents/skills/search-arxiv/` (Codex CLI, Cline, Gemini CLI, Copilot, OpenCode, Warp, Goose, Replit — universal target) -- `~/.continue/skills/search-arxiv/` (Continue) -- `~/.windsurf/skills/search-arxiv/` (Windsurf) -- `<repo-root>/skills/search-arxiv/` (during repo development) - -This skill has no sibling-skill dependencies — it ships its own Python script. - -## Commands - -Run commands via Bash. - -### Search - -```bash -python {{SKILL_DIR}}/search_arxiv.py search "BFT consensus communication complexity" --max-results 10 --categories cs.CR,cs.DC -``` - -Returns JSON array of papers: `arxiv_id`, `title`, `authors`, `year`, `abstract`, `categories`, `pdf_url`, `published`. - -### Download - -```bash -python {{SKILL_DIR}}/search_arxiv.py download 2305.12345 --output-dir reaper-workspace/papers/ -``` - -Downloads the paper PDF. Returns JSON with `path` and `title`. - -### Citations - -```bash -python {{SKILL_DIR}}/search_arxiv.py citations 2305.12345 --max-results 20 -``` - -Returns JSON with `references` (backward citations — what this paper builds on) and `citations` (forward citations — who cites this paper). Each entry has `title`, `authors`, `year`, `arxiv_id`, `url`. - -## Role - -- **Standalone**: Invoked directly by the user to search for papers. -- **Building block**: Called by `/review-literature` and `/investigate` via the underlying Python script. - -## Instructions - -When invoked directly: - -1. Parse the user's query and any flags from the argument. -2. Run the search command via Bash. -3. Format the results as a readable table: - -```markdown -| # | Title | Authors | Year | arXiv ID | Link | Categories | -|---|-------|---------|------|----------|------|------------| -| 1 | ... | ... | ... | ... | [arXiv](https://arxiv.org/abs/XXXX.XXXXX) | ... | -``` - -4. For each highly relevant result, show the abstract excerpt. - -## Quality Criteria - -- Search returns results (graceful error message if API is down or script fails) -- Results are formatted as a readable table with abstract excerpts for top hits -- If the script fails (missing deps, network error), report the error to the caller - -## Dependencies - -Requires `arxiv` and `requests` Python packages: - -```bash -pip install arxiv requests -``` diff --git a/skills/search-arxiv/search_arxiv.py b/skills/search-arxiv/search_arxiv.py deleted file mode 100644 index b41216a..0000000 --- a/skills/search-arxiv/search_arxiv.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -"""Search arXiv for academic papers. - -Usage: - python search_arxiv.py search "query" [--max-results N] [--categories cat1,cat2] - python search_arxiv.py download ARXIV_ID [--output-dir DIR] - python search_arxiv.py citations ARXIV_ID [--max-results N] - -Requires: pip install arxiv requests -""" - -import argparse -import json -import sys -from pathlib import Path - - -def cmd_search(args): - """Search arXiv papers by query.""" - import arxiv - - search = arxiv.Search( - query=args.query, - max_results=args.max_results, - sort_by=arxiv.SortCriterion.Relevance, - ) - - if args.categories: - cats = [c.strip() for c in args.categories.split(",")] - cat_query = " OR ".join(f"cat:{c}" for c in cats) - search.query = f"({args.query}) AND ({cat_query})" - - results = [] - client = arxiv.Client() - for paper in client.results(search): - results.append({ - "arxiv_id": paper.entry_id.split("/abs/")[-1], - "title": paper.title.replace("\n", " "), - "authors": [a.name for a in paper.authors], - "year": paper.published.year, - "abstract": paper.summary.replace("\n", " "), - "categories": paper.categories, - "pdf_url": paper.pdf_url, - "published": paper.published.strftime("%Y-%m-%d"), - }) - - json.dump(results, sys.stdout, indent=2) - print() - - -def cmd_download(args): - """Download a paper PDF by arXiv ID.""" - import arxiv - - search = arxiv.Search(id_list=[args.arxiv_id]) - client = arxiv.Client() - paper = next(client.results(search), None) - if not paper: - print(json.dumps({"error": f"Paper {args.arxiv_id} not found"})) - sys.exit(1) - - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - path = paper.download_pdf(dirpath=str(output_dir)) - print(json.dumps({"path": str(path), "title": paper.title})) - - -def cmd_citations(args): - """Get forward/backward citations via Semantic Scholar API.""" - import requests - - base = "https://api.semanticscholar.org/graph/v1/paper" - arxiv_key = f"ARXIV:{args.arxiv_id}" - - result = {"arxiv_id": args.arxiv_id, "references": [], "citations": []} - - # Backward citations (references) - try: - resp = requests.get( - f"{base}/{arxiv_key}/references", - params={"fields": "title,authors,year,externalIds,url", "limit": args.max_results}, - timeout=15, - ) - if resp.status_code == 200: - for ref in resp.json().get("data", []): - cited = ref.get("citedPaper", {}) - if cited.get("title"): - result["references"].append({ - "title": cited["title"], - "authors": [a["name"] for a in (cited.get("authors") or [])], - "year": cited.get("year"), - "arxiv_id": (cited.get("externalIds") or {}).get("ArXiv"), - "url": cited.get("url"), - }) - except requests.RequestException: - pass - - # Forward citations (who cites this) - try: - resp = requests.get( - f"{base}/{arxiv_key}/citations", - params={"fields": "title,authors,year,externalIds,url", "limit": args.max_results}, - timeout=15, - ) - if resp.status_code == 200: - for cit in resp.json().get("data", []): - citing = cit.get("citingPaper", {}) - if citing.get("title"): - result["citations"].append({ - "title": citing["title"], - "authors": [a["name"] for a in (citing.get("authors") or [])], - "year": citing.get("year"), - "arxiv_id": (citing.get("externalIds") or {}).get("ArXiv"), - "url": citing.get("url"), - }) - except requests.RequestException: - pass - - json.dump(result, sys.stdout, indent=2) - print() - - -def main(): - parser = argparse.ArgumentParser(description="Search arXiv for academic papers") - sub = parser.add_subparsers(dest="command", required=True) - - p_search = sub.add_parser("search", help="Search papers by query") - p_search.add_argument("query", help="Search query") - p_search.add_argument("--max-results", type=int, default=10) - p_search.add_argument("--categories", help="Comma-separated arXiv categories (e.g. cs.CR,cs.DC)") - - p_download = sub.add_parser("download", help="Download paper PDF") - p_download.add_argument("arxiv_id", help="arXiv paper ID (e.g. 2305.12345)") - p_download.add_argument("--output-dir", default=".") - - p_cite = sub.add_parser("citations", help="Get forward/backward citations") - p_cite.add_argument("arxiv_id", help="arXiv paper ID") - p_cite.add_argument("--max-results", type=int, default=20) - - args = parser.parse_args() - {"search": cmd_search, "download": cmd_download, "citations": cmd_citations}[args.command](args) - - -if __name__ == "__main__": - main() diff --git a/skills/search-iacr/SKILL.md b/skills/search-iacr/SKILL.md deleted file mode 100644 index 4e6fc62..0000000 --- a/skills/search-iacr/SKILL.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: search-iacr -description: "Search IACR ePrint archive for cryptography papers, get recent papers, and download PDFs. Use when you need to find cryptography/security papers on ePrint, check recent publications, or download a specific ePrint paper." -user-invocable: true -argument-hint: "<query> [--max-results N]" ---- - -# Search IACR ePrint - -Search the IACR Cryptology ePrint Archive for cryptography and security papers. - -## Usage - -Invoke this skill by name with the query as a quoted string. On slash-command hosts, prefix with `/` (e.g. `/search-iacr "<query>"`). - -``` -search-iacr "threshold signatures" --max-results 15 -``` - -## Path Resolution Protocol - -This skill wraps `search_iacr.py`, which lives in the **same directory as this `SKILL.md`**. **`{{SKILL_DIR}}`** below is a template placeholder — **you MUST substitute it with the absolute install path of this skill before invoking, or the exec will fail.** Common install locations: - -- `~/.claude/skills/search-iacr/` (Claude Code) -- `~/.cursor/skills/search-iacr/` (Cursor) -- `~/.agents/skills/search-iacr/` (Codex CLI, Cline, Gemini CLI, Copilot, OpenCode, Warp, Goose, Replit — universal target) -- `~/.continue/skills/search-iacr/` (Continue) -- `~/.windsurf/skills/search-iacr/` (Windsurf) -- `<repo-root>/skills/search-iacr/` (during repo development) - -This skill has no sibling-skill dependencies — it ships its own Python script. - -## Commands - -Run commands via Bash. - -### Search - -```bash -python {{SKILL_DIR}}/search_iacr.py search "post-quantum threshold signatures" --max-results 10 -``` - -Returns JSON array of papers: `eprint_id`, `title`, `authors`, `year`, `abstract` (for top 5), `pdf_url`, `url`. Top 5 results are enriched with metadata from the paper page. - -### Recent Papers - -```bash -python {{SKILL_DIR}}/search_iacr.py recent --max-results 10 -``` - -Returns the most recently published ePrint papers. - -### Download - -```bash -python {{SKILL_DIR}}/search_iacr.py download 2024/1234 --output-dir reaper-workspace/papers/ -``` - -Downloads the paper PDF. Returns JSON with `path` and `eprint_id`. - -### Get URL - -```bash -python {{SKILL_DIR}}/search_iacr.py url 2024/1234 -``` - -Returns JSON with `url` and `pdf_url` for the paper. - -## Role - -- **Standalone**: Invoked directly by the user to search for papers. -- **Building block**: Called by `/review-literature` and `/investigate` via the underlying Python script. - -## Instructions - -When invoked directly: - -1. Parse the user's query and any flags from the argument. -2. Run the search command via Bash. -3. Format the results as a readable table: - -```markdown -| # | Title | Authors | Year | ePrint ID | Link | -|---|-------|---------|------|-----------|------| -| 1 | ... | ... | ... | ... | [ePrint](https://eprint.iacr.org/YYYY/NNNN) | -``` - -4. For enriched results (top 5), show the abstract excerpt. - -## Quality Criteria - -- Search returns results (graceful error message if API is down or script fails) -- Results are formatted as a readable table with abstract excerpts for enriched hits -- If the script fails (missing deps, network error), report the error to the caller - -## Dependencies - -Requires `requests` and `beautifulsoup4` Python packages: - -```bash -pip install requests beautifulsoup4 -``` diff --git a/skills/search-paper/SKILL.md b/skills/search-paper/SKILL.md new file mode 100644 index 0000000..05c23b7 --- /dev/null +++ b/skills/search-paper/SKILL.md @@ -0,0 +1,185 @@ +--- +name: search-paper +description: "Find papers, download PDFs, traverse citation graphs, and resolve publication venues across arXiv, IACR ePrint, Semantic Scholar, DBLP, and OpenAlex. Use when you need to find papers, trace citations, or determine where a paper was published." +user-invocable: true +argument-hint: "<query> [--source arxiv|iacr] [--max-results N]" +--- + +# Search Paper + +A single skill that wraps five platform drivers — two preprint archives (arXiv, IACR ePrint) and three metadata services (Semantic Scholar, DBLP, OpenAlex). The SKILL.md itself acts as the orchestrator: each script does one thing per platform, and the agent chains them. + +## Usage + +Invoke this skill by name. On slash-command hosts: `/search-paper "<query>"`. + +``` +search-paper "post-quantum threshold signatures" --source iacr --max-results 15 +``` + +## Path Resolution Protocol + +This skill ships five Python scripts in the **same directory as this `SKILL.md`**. The placeholder **`{{SKILL_DIR}}`** below is a template token — **you MUST substitute it with the absolute install path of this skill before invoking, or the exec will fail.** Common install locations: + +- `~/.claude/skills/search-paper/` (Claude Code) +- `~/.cursor/skills/search-paper/` (Cursor) +- `~/.agents/skills/search-paper/` (Codex CLI, Cline, Gemini CLI, Copilot, OpenCode, Warp, Goose, Replit — universal target) +- `~/.continue/skills/search-paper/` (Continue) +- `~/.windsurf/skills/search-paper/` (Windsurf) +- `<repo-root>/skills/search-paper/` (during repo development) + +This skill has no sibling-skill dependencies — it ships its own scripts. + +## Scripts + +Run via Bash. All scripts emit JSON on stdout. + +### `arxiv.py` — arXiv preprint server + +```bash +python {{SKILL_DIR}}/arxiv.py search "BFT consensus communication complexity" --max-results 10 --categories cs.CR,cs.DC +python {{SKILL_DIR}}/arxiv.py recent "threshold signatures" --max-results 10 --categories cs.CR +python {{SKILL_DIR}}/arxiv.py download 2305.12345 --output-dir reaper-workspace/papers/ +python {{SKILL_DIR}}/arxiv.py journal-ref 2305.12345 +``` + +- `search` — array of `{arxiv_id, title, authors, year, abstract, categories, pdf_url, published, journal_ref}` sorted by relevance. +- `recent` — same fields, sorted by submission date (newest first). Requires a query and/or `--categories`. +- `download` — saves PDF, returns `{path, title}`. +- `journal-ref` — author-supplied venue (sparse but authoritative when present). + +### `iacr.py` — IACR ePrint archive + +```bash +python {{SKILL_DIR}}/iacr.py search "threshold signatures" --max-results 10 +python {{SKILL_DIR}}/iacr.py recent --max-results 10 +python {{SKILL_DIR}}/iacr.py download 2024/1234 --output-dir reaper-workspace/papers/ +python {{SKILL_DIR}}/iacr.py url 2024/1234 +python {{SKILL_DIR}}/iacr.py pubinfo 2024/1234 +``` + +- `search` — array of `{eprint_id, title, authors, year, abstract, publication_info, venue, pdf_url, url}`. Top 5 results are enriched with metadata from the paper page (including `publication_info`). +- `recent` — most-recently-posted ePrint papers. +- `download` / `url` — PDF download and URL resolution. +- `pubinfo` — scrapes the "Publication info" line from the paper page (e.g. *"A major revision of CRYPTO 2023"*) and best-effort parses out the venue acronym + year. + +### `semantic_scholar.py` — Semantic Scholar metadata + +```bash +python {{SKILL_DIR}}/semantic_scholar.py venue --arxiv 2305.12345 +python {{SKILL_DIR}}/semantic_scholar.py venue --title "HotStuff: BFT Consensus in the Lens of Blockchain" +python {{SKILL_DIR}}/semantic_scholar.py citations 2305.12345 --max-results 20 +``` + +- `venue` — looks up the publication venue by arXiv ID (preferred when available — exact match) or by title (fuzzy match via `/paper/search/match`). Returns `{found, venue, venue_full, venue_type, year, title, authors}`. +- `citations` — forward (who cites this) + backward (what this builds on) citations. Each entry includes `venue` when known. + +### `dblp.py` — DBLP (CS-focused) + +```bash +python {{SKILL_DIR}}/dblp.py venue "HotStuff: BFT Consensus" --author "Yin" +``` + +- `venue` — title (+ optional author surname) lookup. DBLP is authoritative for CS conference and journal venues. + +### `openalex.py` — OpenAlex (broad coverage) + +```bash +python {{SKILL_DIR}}/openalex.py venue "HotStuff: BFT Consensus in the Lens of Blockchain" +``` + +- `venue` — title-based lookup. Use when DBLP doesn't cover the venue (non-CS, niche workshops, books). + +## Role + +- **Standalone**: Invoked directly by the user to search for papers, trace citations, or resolve a venue. +- **Building block**: Called by `/review-literature` and `/investigate` for structured paper search and venue resolution. + +## Instructions + +When invoked directly: + +1. Parse the user's query and any flags from the argument. +2. Pick the right script(s): + - Crypto/security topic → run `iacr.py search` AND `arxiv.py search --categories cs.CR`. + - General CS topic → run `arxiv.py search` with appropriate categories. + - Citation context → `semantic_scholar.py citations <arxiv_id>`. + - Venue resolution → follow the **Venue Resolution Protocol** below. +3. Format paper results as a readable table: + +```markdown +| # | Title | Authors | Year | Venue | ID | Link | +|---|-------|---------|------|-------|----|------| +| 1 | ... | ... | ... | ... | arXiv:XXXX.XXXXX | [arXiv](https://arxiv.org/abs/XXXX.XXXXX) | +``` + +4. For each highly relevant result, show the abstract excerpt. + +## Venue Resolution Protocol + +A paper's archive ID (arXiv, ePrint) is *not* its publication venue. Resolve the actual venue (CRYPTO, S&P, PODC, …) for every paper that goes into a literature review or report references section. Run the layers in order and **stop at the first success**: + +### Layer 1 — Semantic Scholar (cheapest, highest hit-rate) + +```bash +# arXiv-known papers +python {{SKILL_DIR}}/semantic_scholar.py venue --arxiv <arxiv_id> + +# ePrint-only papers +python {{SKILL_DIR}}/semantic_scholar.py venue --title "<exact title>" +``` + +If `found: true` and `venue` is non-empty → done. Record `source = "semantic_scholar"`. + +### Layer 2 — Author-supplied field on the source archive + +Authors sometimes mark the venue on their own preprint: + +```bash +# arXiv: the journal_ref field +python {{SKILL_DIR}}/arxiv.py journal-ref <arxiv_id> + +# ePrint: the "Publication info" line +python {{SKILL_DIR}}/iacr.py pubinfo <eprint_id> +``` + +If a non-empty `journal_ref` / `publication_info` is returned → done. Record `source = "arxiv_journal_ref"` or `"iacr_pubinfo"`. + +### Layer 3 — DBLP (CS-authoritative title+author search) + +```bash +python {{SKILL_DIR}}/dblp.py venue "<title>" --author "<first author surname>" +``` + +If `found: true` → done. Record `source = "dblp"`. + +### Layer 4 — OpenAlex (broad coverage) + +```bash +python {{SKILL_DIR}}/openalex.py venue "<title>" +``` + +If `found: true` → done. Record `source = "openalex"`. + +### Layer 5 — Preprint-only label + +If all four layers fail, label the entry `(preprint)` rather than silently omitting. Do **not** guess a venue from the topic or author affiliation — an unverified guess is worse than an honest "preprint only". + +### Notes + +- Layers 1 + 2 cover ~80% of papers at near-zero cost. Add layers 3 + 4 only when needed. +- Cache results in your workspace notes — don't re-resolve the same paper across cycles. +- When two sources disagree, prefer the higher-tier source name (Semantic Scholar's `publicationVenue.name` over DBLP's terse acronym, etc.). Record both if confidence is low. + +## Quality Criteria + +- Search returns results (graceful error message if API is down or script fails) +- Results are formatted as a readable table with abstract excerpts for top hits +- For literature reviews and synthesized reports, every cited paper has a resolved venue or an explicit `(preprint)` label — no entry shows only an arXiv/ePrint ID where a venue is expected +- If a script fails (missing deps, network error), report the error to the caller and continue with other layers + +## Dependencies + +```bash +pip install arxiv requests beautifulsoup4 +``` diff --git a/skills/search-paper/arxiv.py b/skills/search-paper/arxiv.py new file mode 100644 index 0000000..7b3e479 --- /dev/null +++ b/skills/search-paper/arxiv.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""arXiv platform driver: search papers, list recent submissions, download PDFs, read journal_ref field. + +Usage: + python arxiv.py search "<query>" [--max-results N] [--categories cat1,cat2] + python arxiv.py recent ["<query>"] [--max-results N] [--categories cat1,cat2] + python arxiv.py download <arxiv_id> [--output-dir DIR] + python arxiv.py journal-ref <arxiv_id> + +Requires: pip install arxiv +""" + +import argparse +import json +import os +import sys +from pathlib import Path + +# This file is named `arxiv.py` to match the platform convention used by the +# other drivers in this skill. When run as a script, Python prepends our own +# directory to sys.path, which would shadow the third-party `arxiv` PyPI +# package. Strip that entry so `import arxiv` resolves to the package. +_here = os.path.dirname(os.path.abspath(__file__)) +sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _here] + + +def cmd_search(args): + """Search arXiv papers by query.""" + import arxiv + + search = arxiv.Search( + query=args.query, + max_results=args.max_results, + sort_by=arxiv.SortCriterion.Relevance, + ) + + if args.categories: + cats = [c.strip() for c in args.categories.split(",")] + cat_query = " OR ".join(f"cat:{c}" for c in cats) + search.query = f"({args.query}) AND ({cat_query})" + + results = [] + client = arxiv.Client() + for paper in client.results(search): + results.append({ + "arxiv_id": paper.entry_id.split("/abs/")[-1], + "title": paper.title.replace("\n", " "), + "authors": [a.name for a in paper.authors], + "year": paper.published.year, + "abstract": paper.summary.replace("\n", " "), + "categories": paper.categories, + "pdf_url": paper.pdf_url, + "published": paper.published.strftime("%Y-%m-%d"), + "journal_ref": paper.journal_ref, + }) + + json.dump(results, sys.stdout, indent=2) + print() + + +def cmd_recent(args): + """Return the most recently submitted arXiv papers matching the query/categories. + + At least one of `query` or `--categories` must be supplied — the arXiv API + does not accept an empty search expression. + """ + import arxiv + + cat_query = None + if args.categories: + cats = [c.strip() for c in args.categories.split(",")] + cat_query = " OR ".join(f"cat:{c}" for c in cats) + + if args.query and cat_query: + query = f"({args.query}) AND ({cat_query})" + elif args.query: + query = args.query + elif cat_query: + query = cat_query + else: + print(json.dumps({"error": "recent requires a query or --categories"})) + sys.exit(2) + + search = arxiv.Search( + query=query, + max_results=args.max_results, + sort_by=arxiv.SortCriterion.SubmittedDate, + ) + + results = [] + client = arxiv.Client() + for paper in client.results(search): + results.append({ + "arxiv_id": paper.entry_id.split("/abs/")[-1], + "title": paper.title.replace("\n", " "), + "authors": [a.name for a in paper.authors], + "year": paper.published.year, + "abstract": paper.summary.replace("\n", " "), + "categories": paper.categories, + "pdf_url": paper.pdf_url, + "published": paper.published.strftime("%Y-%m-%d"), + "journal_ref": paper.journal_ref, + }) + + json.dump(results, sys.stdout, indent=2) + print() + + +def cmd_download(args): + """Download a paper PDF by arXiv ID.""" + import arxiv + + search = arxiv.Search(id_list=[args.arxiv_id]) + client = arxiv.Client() + paper = next(client.results(search), None) + if not paper: + print(json.dumps({"error": f"Paper {args.arxiv_id} not found"})) + sys.exit(1) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + path = paper.download_pdf(dirpath=str(output_dir)) + print(json.dumps({"path": str(path), "title": paper.title})) + + +def cmd_journal_ref(args): + """Return the author-supplied journal_ref field for an arXiv paper. + + arXiv lets authors set this when their preprint has been accepted somewhere. + Sparse but authoritative when present. + """ + import arxiv + + search = arxiv.Search(id_list=[args.arxiv_id]) + client = arxiv.Client() + paper = next(client.results(search), None) + if not paper: + print(json.dumps({"error": f"Paper {args.arxiv_id} not found"})) + sys.exit(1) + + json.dump({ + "arxiv_id": args.arxiv_id, + "journal_ref": paper.journal_ref, + "title": paper.title.replace("\n", " "), + "authors": [a.name for a in paper.authors], + "year": paper.published.year if paper.published else None, + }, sys.stdout, indent=2) + print() + + +def main(): + parser = argparse.ArgumentParser(description="arXiv platform driver") + sub = parser.add_subparsers(dest="command", required=True) + + p_search = sub.add_parser("search", help="Search papers by query") + p_search.add_argument("query", help="Search query") + p_search.add_argument("--max-results", type=int, default=10) + p_search.add_argument("--categories", help="Comma-separated arXiv categories (e.g. cs.CR,cs.DC)") + + p_recent = sub.add_parser("recent", help="Get recently submitted papers (by date)") + p_recent.add_argument("query", nargs="?", default=None, help="Optional query to scope the recent feed") + p_recent.add_argument("--max-results", type=int, default=10) + p_recent.add_argument("--categories", help="Comma-separated arXiv categories (e.g. cs.CR,cs.DC)") + + p_download = sub.add_parser("download", help="Download paper PDF") + p_download.add_argument("arxiv_id", help="arXiv paper ID (e.g. 2305.12345)") + p_download.add_argument("--output-dir", default=".") + + p_jref = sub.add_parser("journal-ref", help="Read arXiv journal_ref field (author-supplied venue)") + p_jref.add_argument("arxiv_id", help="arXiv paper ID") + + args = parser.parse_args() + { + "search": cmd_search, + "recent": cmd_recent, + "download": cmd_download, + "journal-ref": cmd_journal_ref, + }[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/skills/search-paper/dblp.py b/skills/search-paper/dblp.py new file mode 100644 index 0000000..ea8e637 --- /dev/null +++ b/skills/search-paper/dblp.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""DBLP driver: title+author venue lookup. Authoritative for CS venues. + +Usage: + python dblp.py venue "<title>" [--author "<surname>"] + +Requires: pip install requests +""" + +import argparse +import json +import re +import sys + + +API = "https://dblp.org/search/publ/api" +HTTP_TIMEOUT = 15 + + +def _normalize(text): + if not text: + return None + return re.sub(r"\s+", " ", text).strip() or None + + +def cmd_venue(args): + """Search DBLP by title (+ optional author) and return the top hit's venue.""" + import requests + + query = args.title + if args.author: + query = f"{args.title} {args.author}" + + try: + resp = requests.get( + API, + params={"q": query, "format": "json", "h": 3}, + timeout=HTTP_TIMEOUT, + ) + if resp.status_code != 200: + _emit(args.title, None) + return + hits = (resp.json().get("result", {}).get("hits", {}).get("hit", []) or []) + except (requests.RequestException, ValueError): + _emit(args.title, None) + return + + for hit in hits: + info = hit.get("info") or {} + venue = _normalize(info.get("venue")) + if not venue: + continue + year = info.get("year") + venue_with_year = f"{venue} {year}" if year else venue + json.dump({ + "query": {"title": args.title, "author": args.author}, + "found": True, + "venue": venue_with_year, + "venue_full": venue, + "venue_type": info.get("type"), + "year": int(year) if (year or "").isdigit() else None, + "title": _normalize(info.get("title")), + "url": info.get("url"), + }, sys.stdout, indent=2) + print() + return + + _emit(args.title, None) + + +def _emit(title, _): + json.dump({ + "query": {"title": title}, + "found": False, + "venue": None, + "venue_full": None, + "venue_type": None, + "year": None, + "title": None, + "url": None, + }, sys.stdout, indent=2) + print() + + +def main(): + parser = argparse.ArgumentParser(description="DBLP driver") + sub = parser.add_subparsers(dest="command", required=True) + + p_venue = sub.add_parser("venue", help="Look up venue by title (+ author)") + p_venue.add_argument("title", help="Paper title") + p_venue.add_argument("--author", help="Author surname (improves disambiguation)") + + args = parser.parse_args() + {"venue": cmd_venue}[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/skills/search-iacr/search_iacr.py b/skills/search-paper/iacr.py similarity index 60% rename from skills/search-iacr/search_iacr.py rename to skills/search-paper/iacr.py index 1575040..67e0271 100644 --- a/skills/search-iacr/search_iacr.py +++ b/skills/search-paper/iacr.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -"""Search IACR ePrint archive for cryptography papers. +"""IACR ePrint platform driver: search, recent, download, url, pubinfo. Usage: - python search_iacr.py search "query" [--max-results N] - python search_iacr.py recent [--max-results N] - python search_iacr.py download EPRINT_ID [--output-dir DIR] - python search_iacr.py url EPRINT_ID + python iacr.py search "<query>" [--max-results N] + python iacr.py recent [--max-results N] + python iacr.py download <eprint_id> [--output-dir DIR] + python iacr.py url <eprint_id> + python iacr.py pubinfo <eprint_id> Requires: pip install requests beautifulsoup4 """ @@ -19,6 +20,15 @@ EPRINT_BASE = "https://eprint.iacr.org" +# Acronyms recognized in ePrint "Publication info" lines. +KNOWN_VENUES = ( + "CRYPTO", "EUROCRYPT", "ASIACRYPT", "TCC", "PKC", "CHES", "FSE", + "CCS", "S&P", "USENIX Security", "USENIX", "NDSS", "Oakland", + "FC", "ESORICS", "ACNS", "SCN", "PETS", "AFT", + "ICALP", "STOC", "FOCS", "PODC", "DISC", "OPODIS", "DSN", + "JoC", "TIFS", "TDSC", "DCC", +) + def _parse_search_results(html, max_results): """Parse ePrint search results HTML into structured data.""" @@ -27,11 +37,6 @@ def _parse_search_results(html, max_results): soup = BeautifulSoup(html, "html.parser") results = [] - # ePrint search results are in <dl> with <dt> containing the ID and <dd> containing details - # Try the newer format first: papers are in divs or list items - # The search page returns results as a list of papers - - # Pattern: look for links to /YEAR/NNN paper_links = soup.find_all("a", href=re.compile(r"^/\d{4}/\d+$")) seen = set() @@ -47,19 +52,15 @@ def _parse_search_results(html, max_results): title = link.get_text(strip=True) if not title or title == eprint_id: - # Try to find title in surrounding context parent = link.find_parent(["div", "li", "dd", "tr", "p"]) if parent: - # Look for bold or strong text as title bold = parent.find(["b", "strong"]) if bold: title = bold.get_text(strip=True) - # Try to extract authors from surrounding context authors = [] parent = link.find_parent(["div", "li", "dd", "tr", "p"]) if parent: - # Authors often appear in <em> or <i> tags, or after "by" em = parent.find(["em", "i"]) if em: authors_text = em.get_text(strip=True) @@ -79,6 +80,57 @@ def _parse_search_results(html, max_results): return results +def _extract_publication_info(soup): + """Extract the 'Publication info' free-form line from a paper page, if any. + + Format varies: 'A minor revision of an IACR publication in CRYPTO 2023', + 'Published elsewhere. SCN 2022', 'Conference: ASIACRYPT 2024', etc. + Returns the raw line plus best-effort parsed venue/year. + """ + raw = None + + # Newer template uses <dt>Publication info</dt><dd>...</dd> + for dt in soup.find_all("dt"): + if "publication info" in dt.get_text(strip=True).lower(): + dd = dt.find_next_sibling("dd") + if dd: + raw = dd.get_text(" ", strip=True) + break + + # Older template: a <p> or <b> introducing the line + if not raw: + for tag in soup.find_all(["b", "strong"]): + if "publication info" in tag.get_text(strip=True).lower(): + parent = tag.find_parent(["p", "div", "dd"]) + if parent: + text = parent.get_text(" ", strip=True) + raw = re.sub(r"^.*?publication info[:\s]*", "", text, flags=re.IGNORECASE).strip() + break + + # Generic fallback: any element directly containing the literal string + if not raw: + match = re.search(r"Publication info[:\s]+([^\n<]+)", soup.get_text("\n")) + if match: + raw = match.group(1).strip() + + if not raw: + return {"raw": None, "venue": None, "year": None} + + venue = None + for v in KNOWN_VENUES: + if re.search(rf"\b{re.escape(v)}\b", raw, re.IGNORECASE): + venue = v + break + + year = None + m = re.search(r"\b(19|20)\d{2}\b", raw) + if m: + year = int(m.group(0)) + + venue_with_year = f"{venue} {year}" if venue and year else venue + return {"raw": raw, "venue": venue_with_year, "year": year} + + def _fetch_paper_page(eprint_id): """Fetch and parse a single ePrint paper page for metadata.""" import requests @@ -92,35 +144,36 @@ def _fetch_paper_page(eprint_id): authors = [] abstract = "" - # Title is typically in <h3> or page title title_el = soup.find("h3") if title_el: title = title_el.get_text(strip=True) elif soup.title: title = soup.title.get_text(strip=True).replace("ePrint Report – ", "") - # Look for metadata in <div class="paper-..."> or <p> tags for p in soup.find_all("p"): text = p.get_text(strip=True) if text.startswith("Abstract:") or text.startswith("Abstract."): abstract = text.split(":", 1)[-1].strip() if ":" in text else text.split(".", 1)[-1].strip() - # Authors from meta tags for meta in soup.find_all("meta", {"name": "citation_author"}): content = meta.get("content", "") if content: authors.append(content) - # Title from meta tag meta_title = soup.find("meta", {"name": "citation_title"}) if meta_title and meta_title.get("content"): title = meta_title["content"] + pub = _extract_publication_info(soup) + return { "eprint_id": eprint_id, "title": title, "authors": authors, "abstract": abstract, + "publication_info": pub["raw"], + "venue": pub["venue"], + "venue_year": pub["year"], "pdf_url": f"{EPRINT_BASE}/{eprint_id}.pdf", "url": f"{EPRINT_BASE}/{eprint_id}", } @@ -138,7 +191,6 @@ def cmd_search(args): resp.raise_for_status() results = _parse_search_results(resp.text, args.max_results) - # Enrich top results with metadata from individual pages enriched = [] for r in results[:min(5, len(results))]: try: @@ -148,7 +200,6 @@ def cmd_search(args): except Exception: enriched.append(r) - # Add remaining without enrichment enriched.extend(results[5:]) json.dump(enriched, sys.stdout, indent=2) print() @@ -192,8 +243,26 @@ def cmd_url(args): })) +def cmd_pubinfo(args): + """Fetch the 'Publication info' line from an ePrint paper page. + + ePrint authors mark the venue here (e.g., 'A major revision of CRYPTO 2023'). + Returns raw line + best-effort parsed venue/year. + """ + detail = _fetch_paper_page(args.eprint_id) + json.dump({ + "eprint_id": args.eprint_id, + "title": detail.get("title"), + "authors": detail.get("authors") or [], + "publication_info": detail.get("publication_info"), + "venue": detail.get("venue"), + "venue_year": detail.get("venue_year"), + }, sys.stdout, indent=2) + print() + + def main(): - parser = argparse.ArgumentParser(description="Search IACR ePrint archive") + parser = argparse.ArgumentParser(description="IACR ePrint platform driver") sub = parser.add_subparsers(dest="command", required=True) p_search = sub.add_parser("search", help="Search papers by query") @@ -210,8 +279,17 @@ def main(): p_url = sub.add_parser("url", help="Get paper URL") p_url.add_argument("eprint_id", help="ePrint ID") + p_pub = sub.add_parser("pubinfo", help="Read ePrint 'Publication info' field (author-supplied venue)") + p_pub.add_argument("eprint_id", help="ePrint ID") + args = parser.parse_args() - {"search": cmd_search, "recent": cmd_recent, "download": cmd_download, "url": cmd_url}[args.command](args) + { + "search": cmd_search, + "recent": cmd_recent, + "download": cmd_download, + "url": cmd_url, + "pubinfo": cmd_pubinfo, + }[args.command](args) if __name__ == "__main__": diff --git a/skills/search-paper/openalex.py b/skills/search-paper/openalex.py new file mode 100644 index 0000000..c8b6ad7 --- /dev/null +++ b/skills/search-paper/openalex.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""OpenAlex driver: title-based venue lookup. Broad coverage beyond CS. + +Usage: + python openalex.py venue "<title>" + +Requires: pip install requests +""" + +import argparse +import json +import re +import sys + + +API = "https://api.openalex.org/works" +HTTP_TIMEOUT = 15 + + +def _normalize(text): + if not text: + return None + return re.sub(r"\s+", " ", text).strip() or None + + +def cmd_venue(args): + """Search OpenAlex by title and return the top hit's venue.""" + import requests + + try: + resp = requests.get( + API, + params={"search": args.title, "per-page": 1}, + timeout=HTTP_TIMEOUT, + ) + if resp.status_code != 200: + _emit(args.title, None) + return + results = resp.json().get("results", []) + except (requests.RequestException, ValueError): + _emit(args.title, None) + return + + if not results: + _emit(args.title, None) + return + + work = results[0] + # Newer schema: primary_location.source.display_name + # Older schema: host_venue.display_name + source = (work.get("primary_location") or {}).get("source") or {} + venue_full = _normalize(source.get("display_name")) or _normalize( + (work.get("host_venue") or {}).get("display_name") + ) + venue_type = source.get("type") or (work.get("host_venue") or {}).get("type") + year = work.get("publication_year") + + if not venue_full: + _emit(args.title, None) + return + + venue_with_year = f"{venue_full} {year}" if year else venue_full + json.dump({ + "query": {"title": args.title}, + "found": True, + "venue": venue_with_year, + "venue_full": venue_full, + "venue_type": venue_type, + "year": year, + "title": _normalize(work.get("title") or work.get("display_name")), + "url": work.get("doi") or work.get("id"), + }, sys.stdout, indent=2) + print() + + +def _emit(title, _): + json.dump({ + "query": {"title": title}, + "found": False, + "venue": None, + "venue_full": None, + "venue_type": None, + "year": None, + "title": None, + "url": None, + }, sys.stdout, indent=2) + print() + + +def main(): + parser = argparse.ArgumentParser(description="OpenAlex driver") + sub = parser.add_subparsers(dest="command", required=True) + + p_venue = sub.add_parser("venue", help="Look up venue by title") + p_venue.add_argument("title", help="Paper title") + + args = parser.parse_args() + {"venue": cmd_venue}[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/skills/search-paper/semantic_scholar.py b/skills/search-paper/semantic_scholar.py new file mode 100644 index 0000000..6015064 --- /dev/null +++ b/skills/search-paper/semantic_scholar.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Semantic Scholar driver: venue lookup and citation graph. + +Usage: + python semantic_scholar.py venue --arxiv <arxiv_id> + python semantic_scholar.py venue --title "<title>" [--author "<surname>"] + python semantic_scholar.py citations <arxiv_id> [--max-results N] + +Requires: pip install requests +""" + +import argparse +import json +import re +import sys + + +BASE = "https://api.semanticscholar.org/graph/v1/paper" +HTTP_TIMEOUT = 15 + + +def _normalize(text): + if not text: + return None + return re.sub(r"\s+", " ", text).strip() or None + + +def _emit_venue(query, data): + """Format a Semantic Scholar paper record into a venue response.""" + if not data: + json.dump({ + "query": query, + "found": False, + "venue": None, + "venue_full": None, + "venue_type": None, + "year": None, + "title": None, + "authors": [], + }, sys.stdout, indent=2) + print() + return + + pub_venue = data.get("publicationVenue") or {} + venue_full = _normalize(pub_venue.get("name")) + venue_short = _normalize(data.get("venue")) or venue_full + + json.dump({ + "query": query, + "found": True, + "venue": venue_short, + "venue_full": venue_full, + "venue_type": pub_venue.get("type"), + "year": data.get("year"), + "title": _normalize(data.get("title")), + "authors": [a.get("name") for a in (data.get("authors") or []) if a.get("name")], + }, sys.stdout, indent=2) + print() + + +def cmd_venue(args): + """Look up venue + paper metadata for an arXiv ID or by title match.""" + import requests + + fields = "title,authors,year,venue,publicationVenue,externalIds" + + if args.arxiv: + url = f"{BASE}/ARXIV:{args.arxiv}" + try: + resp = requests.get(url, params={"fields": fields}, timeout=HTTP_TIMEOUT) + data = resp.json() if resp.status_code == 200 else None + except (requests.RequestException, ValueError): + data = None + _emit_venue({"arxiv": args.arxiv}, data) + return + + if args.title: + url = f"{BASE}/search/match" + params = {"query": args.title, "fields": fields} + try: + resp = requests.get(url, params=params, timeout=HTTP_TIMEOUT) + payload = resp.json() if resp.status_code == 200 else {} + matches = payload.get("data") or [] + data = matches[0] if matches else None + except (requests.RequestException, ValueError): + data = None + _emit_venue({"title": args.title}, data) + return + + print(json.dumps({"error": "venue requires --arxiv or --title"})) + sys.exit(2) + + +def cmd_citations(args): + """Forward + backward citations for an arXiv paper.""" + import requests + + arxiv_key = f"ARXIV:{args.arxiv_id}" + fields = "title,authors,year,externalIds,url,venue" + result = {"arxiv_id": args.arxiv_id, "references": [], "citations": []} + + try: + resp = requests.get( + f"{BASE}/{arxiv_key}/references", + params={"fields": fields, "limit": args.max_results}, + timeout=HTTP_TIMEOUT, + ) + if resp.status_code == 200: + for ref in resp.json().get("data", []): + cited = ref.get("citedPaper", {}) + if cited.get("title"): + result["references"].append({ + "title": cited["title"], + "authors": [a["name"] for a in (cited.get("authors") or [])], + "year": cited.get("year"), + "venue": cited.get("venue"), + "arxiv_id": (cited.get("externalIds") or {}).get("ArXiv"), + "url": cited.get("url"), + }) + except requests.RequestException: + pass + + try: + resp = requests.get( + f"{BASE}/{arxiv_key}/citations", + params={"fields": fields, "limit": args.max_results}, + timeout=HTTP_TIMEOUT, + ) + if resp.status_code == 200: + for cit in resp.json().get("data", []): + citing = cit.get("citingPaper", {}) + if citing.get("title"): + result["citations"].append({ + "title": citing["title"], + "authors": [a["name"] for a in (citing.get("authors") or [])], + "year": citing.get("year"), + "venue": citing.get("venue"), + "arxiv_id": (citing.get("externalIds") or {}).get("ArXiv"), + "url": citing.get("url"), + }) + except requests.RequestException: + pass + + json.dump(result, sys.stdout, indent=2) + print() + + +def main(): + parser = argparse.ArgumentParser(description="Semantic Scholar driver") + sub = parser.add_subparsers(dest="command", required=True) + + p_venue = sub.add_parser("venue", help="Look up venue by arXiv ID or by title") + p_venue.add_argument("--arxiv", help="arXiv paper ID") + p_venue.add_argument("--title", help="Paper title (uses /paper/search/match)") + p_venue.add_argument("--author", help="Optional author surname (currently informational)") + + p_cite = sub.add_parser("citations", help="Get forward/backward citations") + p_cite.add_argument("arxiv_id", help="arXiv paper ID") + p_cite.add_argument("--max-results", type=int, default=20) + + args = parser.parse_args() + {"venue": cmd_venue, "citations": cmd_citations}[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/skills/synthesize/SKILL.md b/skills/synthesize/SKILL.md index 03b0f6d..35141c4 100644 --- a/skills/synthesize/SKILL.md +++ b/skills/synthesize/SKILL.md @@ -138,7 +138,11 @@ For counterexamples and attacks, use a concrete execution trace: ## References -[Papers cited during the investigation, formatted consistently. For each paper, attach a link to its PDF (e.g., arXiv, IACR ePrint, or publisher PDF URL).] +[Papers cited during the investigation, formatted consistently. For each paper, include the **publication venue** (e.g., CRYPTO 2023, S&P 2024, J. Cryptology Vol 36) — not just an arXiv or ePrint ID. The venue should already be resolved in `notes/literature.md`'s `Venue` column from the `/review-literature` step; copy it through. For papers without a known venue, label as `(preprint)` and include the archive URL. Also attach a link to the PDF (arXiv, IACR ePrint, DOI, or publisher URL). + +Example formats: +- Author, A., Author, B. *Title.* CRYPTO 2023, pp. XXX–YYY. [arXiv:XXXX.XXXXX](https://arxiv.org/abs/XXXX.XXXXX) +- Author, C. *Title.* (preprint) IACR ePrint 2024/1234. [ePrint](https://eprint.iacr.org/2024/1234)] ## Appendix A: Investigation Log @@ -180,3 +184,4 @@ If `current-understanding.md` is empty or missing, reconstruct from `results.md` - The paper is self-contained — a reader unfamiliar with the investigation can follow the argument - No chronological narration ("first we tried X, then we tried Y") — present the clean logical chain - Open questions are stated as conjectures or precise problems, not vague future work +- Every entry in the References section names a real publication venue or is explicitly labeled `(preprint)` — never just an arXiv/ePrint ID where a venue is expected diff --git a/tests/test_search_arxiv.py b/tests/test_search_arxiv.py deleted file mode 100644 index 3855212..0000000 --- a/tests/test_search_arxiv.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for search_arxiv.py — validates CLI interface and output format. - -Uses real API calls (integration tests). Requires network access. -""" - -import json -import subprocess -import sys - -SCRIPT = "skills/search-arxiv/search_arxiv.py" -PYTHON = sys.executable - - -def run(args): - result = subprocess.run( - [PYTHON, SCRIPT] + args, - capture_output=True, text=True, timeout=30, - ) - return result - - -def test_search_returns_json_array(): - r = run(["search", "Byzantine fault tolerance", "--max-results", "3"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert isinstance(data, list) - assert len(data) > 0 - assert len(data) <= 3 - - -def test_search_result_fields(): - r = run(["search", "threshold signatures", "--max-results", "1"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - paper = data[0] - for field in ["arxiv_id", "title", "authors", "year", "abstract", "pdf_url"]: - assert field in paper, f"Missing field: {field}" - assert isinstance(paper["authors"], list) - assert isinstance(paper["year"], int) - assert paper["title"] # non-empty - - -def test_search_with_categories(): - r = run(["search", "consensus protocol", "--max-results", "2", "--categories", "cs.CR,cs.DC"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert len(data) > 0 - - -def test_citations_returns_refs_and_cites(): - """Test citation graph for a well-known paper (HotStuff).""" - r = run(["citations", "1803.05069", "--max-results", "5"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert "references" in data - assert "citations" in data - assert isinstance(data["references"], list) - assert isinstance(data["citations"], list) - # HotStuff is well-cited, should have both - assert len(data["references"]) > 0 or len(data["citations"]) > 0 - - -def test_search_empty_query_returns_results(): - """Even a broad query should return something.""" - r = run(["search", "cryptography", "--max-results", "2"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert len(data) > 0 - - -def test_cli_help(): - r = run(["--help"]) - assert r.returncode == 0 - assert "search" in r.stdout.lower() diff --git a/tests/test_search_iacr.py b/tests/test_search_iacr.py deleted file mode 100644 index f48311a..0000000 --- a/tests/test_search_iacr.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Tests for search_iacr.py — validates CLI interface and output format. - -Uses real API calls (integration tests). Requires network access. -""" - -import json -import subprocess -import sys - -SCRIPT = "skills/search-iacr/search_iacr.py" -PYTHON = sys.executable - - -def run(args): - result = subprocess.run( - [PYTHON, SCRIPT] + args, - capture_output=True, text=True, timeout=30, - ) - return result - - -def test_search_returns_json_array(): - r = run(["search", "threshold signatures", "--max-results", "3"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert isinstance(data, list) - assert len(data) > 0 - - -def test_search_result_fields(): - r = run(["search", "post-quantum", "--max-results", "1"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert len(data) > 0 - paper = data[0] - for field in ["eprint_id", "title", "pdf_url", "url"]: - assert field in paper, f"Missing field: {field}" - assert paper["eprint_id"] # non-empty - assert "/" in paper["eprint_id"] # format: YYYY/NNNN - - -def test_url_command(): - r = run(["url", "2024/1234"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert data["eprint_id"] == "2024/1234" - assert "eprint.iacr.org/2024/1234" in data["url"] - assert data["pdf_url"].endswith(".pdf") - - -def test_recent_returns_results(): - r = run(["recent", "--max-results", "3"]) - assert r.returncode == 0, f"stderr: {r.stderr}" - data = json.loads(r.stdout) - assert isinstance(data, list) - # Recent might be empty if the scraping format changed, but shouldn't error - # We just check it returns valid JSON - - -def test_cli_help(): - r = run(["--help"]) - assert r.returncode == 0 - assert "search" in r.stdout.lower() diff --git a/tests/test_search_paper.py b/tests/test_search_paper.py new file mode 100644 index 0000000..f7a21ce --- /dev/null +++ b/tests/test_search_paper.py @@ -0,0 +1,227 @@ +"""Tests for the /search-paper skill scripts — validates CLI interfaces and output formats. + +Uses real API calls (integration tests). Requires network access. +""" + +import json +import subprocess +import sys + +SKILL_DIR = "skills/search-paper" +PYTHON = sys.executable + + +def run(script, args, timeout=30): + result = subprocess.run( + [PYTHON, f"{SKILL_DIR}/{script}"] + args, + capture_output=True, text=True, timeout=timeout, + ) + return result + + +# --------------------------------------------------------------------------- +# arxiv.py +# --------------------------------------------------------------------------- + +def test_arxiv_search_returns_json_array(): + r = run("arxiv.py", ["search", "Byzantine fault tolerance", "--max-results", "3"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert isinstance(data, list) + assert 0 < len(data) <= 3 + + +def test_arxiv_search_result_fields(): + r = run("arxiv.py", ["search", "threshold signatures", "--max-results", "1"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + paper = data[0] + for field in ["arxiv_id", "title", "authors", "year", "abstract", "pdf_url", "journal_ref"]: + assert field in paper, f"Missing field: {field}" + assert isinstance(paper["authors"], list) + assert isinstance(paper["year"], int) + assert paper["title"] + + +def test_arxiv_search_with_categories(): + r = run("arxiv.py", ["search", "consensus protocol", "--max-results", "2", "--categories", "cs.CR,cs.DC"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert len(data) > 0 + + +def test_arxiv_journal_ref_returns_field(): + """HotStuff (1803.05069) has been published; journal_ref may or may not be set + depending on author choice, but the script must always return a JSON object + with the expected keys.""" + r = run("arxiv.py", ["journal-ref", "1803.05069"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + for field in ["arxiv_id", "journal_ref", "title", "authors", "year"]: + assert field in data, f"Missing field: {field}" + assert data["arxiv_id"] == "1803.05069" + + +def test_arxiv_recent_returns_results(): + """recent must return a JSON array sorted by submission date. At least one + of query/--categories is required; we pass --categories to scope the feed.""" + r = run("arxiv.py", ["recent", "--max-results", "3", "--categories", "cs.CR"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert isinstance(data, list) + assert 0 < len(data) <= 3 + # Results should be sorted newest-first by `published`. + dates = [p["published"] for p in data] + assert dates == sorted(dates, reverse=True), f"recent results not date-sorted: {dates}" + + +def test_arxiv_recent_requires_query_or_categories(): + """recent with neither a query nor --categories must exit non-zero with a + clear error — arXiv's API rejects empty search expressions.""" + r = run("arxiv.py", ["recent", "--max-results", "1"]) + assert r.returncode != 0 + err_payload = (r.stdout or "") + (r.stderr or "") + assert "query" in err_payload.lower() or "categories" in err_payload.lower() + + +def test_arxiv_cli_help(): + r = run("arxiv.py", ["--help"]) + assert r.returncode == 0 + assert "search" in r.stdout.lower() + assert "recent" in r.stdout.lower() + assert "journal-ref" in r.stdout.lower() + + +# --------------------------------------------------------------------------- +# iacr.py +# --------------------------------------------------------------------------- + +def test_iacr_search_returns_json_array(): + r = run("iacr.py", ["search", "threshold signatures", "--max-results", "3"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert isinstance(data, list) + assert len(data) > 0 + + +def test_iacr_search_result_fields(): + r = run("iacr.py", ["search", "post-quantum", "--max-results", "1"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert len(data) > 0 + paper = data[0] + for field in ["eprint_id", "title", "pdf_url", "url"]: + assert field in paper, f"Missing field: {field}" + assert paper["eprint_id"] + assert "/" in paper["eprint_id"] + + +def test_iacr_url_command(): + r = run("iacr.py", ["url", "2024/1234"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert data["eprint_id"] == "2024/1234" + assert "eprint.iacr.org/2024/1234" in data["url"] + assert data["pdf_url"].endswith(".pdf") + + +def test_iacr_pubinfo_returns_fields(): + """The pubinfo endpoint must always return a JSON object with the expected + keys, even if the paper page has no Publication info line.""" + r = run("iacr.py", ["pubinfo", "2024/1234"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + for field in ["eprint_id", "title", "authors", "publication_info", "venue", "venue_year"]: + assert field in data, f"Missing field: {field}" + assert data["eprint_id"] == "2024/1234" + + +def test_iacr_recent_returns_results(): + r = run("iacr.py", ["recent", "--max-results", "3"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert isinstance(data, list) + + +def test_iacr_cli_help(): + r = run("iacr.py", ["--help"]) + assert r.returncode == 0 + assert "search" in r.stdout.lower() + assert "pubinfo" in r.stdout.lower() + + +# --------------------------------------------------------------------------- +# semantic_scholar.py +# --------------------------------------------------------------------------- + +def test_s2_venue_by_arxiv_returns_fields(): + """HotStuff is published at PODC 2019 — venue should resolve. But we tolerate + transient API failures by only checking that the expected keys are present.""" + r = run("semantic_scholar.py", ["venue", "--arxiv", "1803.05069"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + for field in ["query", "found", "venue", "venue_full", "year", "title", "authors"]: + assert field in data, f"Missing field: {field}" + + +def test_s2_venue_by_title_returns_fields(): + r = run("semantic_scholar.py", ["venue", "--title", "HotStuff: BFT Consensus in the Lens of Blockchain"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + for field in ["query", "found", "venue", "title"]: + assert field in data, f"Missing field: {field}" + + +def test_s2_citations_returns_refs_and_cites(): + r = run("semantic_scholar.py", ["citations", "1803.05069", "--max-results", "5"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert "references" in data + assert "citations" in data + assert isinstance(data["references"], list) + assert isinstance(data["citations"], list) + + +def test_s2_cli_help(): + r = run("semantic_scholar.py", ["--help"]) + assert r.returncode == 0 + assert "venue" in r.stdout.lower() + assert "citations" in r.stdout.lower() + + +# --------------------------------------------------------------------------- +# dblp.py +# --------------------------------------------------------------------------- + +def test_dblp_venue_returns_fields(): + """DBLP indexes most CS papers — HotStuff should resolve. Tolerate transient + failures by only checking that the expected JSON shape comes back.""" + r = run("dblp.py", ["venue", "HotStuff: BFT Consensus", "--author", "Yin"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + for field in ["query", "found", "venue", "venue_full", "year", "title"]: + assert field in data, f"Missing field: {field}" + + +def test_dblp_cli_help(): + r = run("dblp.py", ["--help"]) + assert r.returncode == 0 + assert "venue" in r.stdout.lower() + + +# --------------------------------------------------------------------------- +# openalex.py +# --------------------------------------------------------------------------- + +def test_openalex_venue_returns_fields(): + r = run("openalex.py", ["venue", "HotStuff: BFT Consensus in the Lens of Blockchain"]) + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + for field in ["query", "found", "venue", "venue_full", "year", "title"]: + assert field in data, f"Missing field: {field}" + + +def test_openalex_cli_help(): + r = run("openalex.py", ["--help"]) + assert r.returncode == 0 + assert "venue" in r.stdout.lower() diff --git a/tests/test_skills_structure.py b/tests/test_skills_structure.py index 3d2da30..35e320b 100644 --- a/tests/test_skills_structure.py +++ b/tests/test_skills_structure.py @@ -21,8 +21,7 @@ "critique": "skills/critique/SKILL.md", "synthesize": "skills/synthesize/SKILL.md", # H2 - "search-arxiv": "skills/search-arxiv/SKILL.md", - "search-iacr": "skills/search-iacr/SKILL.md", + "search-paper": "skills/search-paper/SKILL.md", } EXPECTED_REFERENCES = [ @@ -32,8 +31,11 @@ ] EXPECTED_SCRIPTS = [ - "skills/search-arxiv/search_arxiv.py", - "skills/search-iacr/search_iacr.py", + "skills/search-paper/arxiv.py", + "skills/search-paper/iacr.py", + "skills/search-paper/semantic_scholar.py", + "skills/search-paper/dblp.py", + "skills/search-paper/openalex.py", ] @@ -101,18 +103,30 @@ def test_marketplace_json_lists_all_skills(): assert not extra, f"marketplace.json lists unknown skills: {extra}" -def test_review_literature_references_search_scripts(): - """H2: review-literature should reference the search scripts.""" +def test_review_literature_delegates_to_search_paper(): + """review-literature must delegate paper search, download, citation graph, + and venue resolution to the /search-paper skill rather than invoking its + scripts directly, and must stay fully path-agnostic.""" content = Path("skills/review-literature/SKILL.md").read_text() - assert "search_arxiv.py" in content, "review-literature doesn't reference search_arxiv.py" - assert "search_iacr.py" in content, "review-literature doesn't reference search_iacr.py" + assert "/search-paper" in content, "review-literature doesn't reference the /search-paper skill" + # Must not reach into any platform driver by name — those are /search-paper's concern. + for script in ("arxiv.py", "iacr.py", "semantic_scholar.py", "dblp.py", "openalex.py"): + assert script not in content, ( + f"review-literature references {script} directly; delegate to /search-paper instead" + ) + # Must not carry any SKILL_DIR placeholders — delegation removed the need for them. + placeholder_pattern = re.compile(r"\{\{[A-Z_]*SKILL_DIR\}\}") + assert not placeholder_pattern.search(content), ( + "review-literature still uses {{*_SKILL_DIR}} placeholders; after the /search-paper " + "delegation refactor it must be fully path-agnostic" + ) def test_investigate_references_search_scripts(): """H2: investigate should reference the search scripts for mid-cycle search.""" content = Path("skills/investigate/SKILL.md").read_text() - assert "search_iacr.py" in content, "investigate doesn't reference search_iacr.py" - assert "search_arxiv.py" in content, "investigate doesn't reference search_arxiv.py" + assert "iacr.py" in content, "investigate doesn't reference iacr.py" + assert "arxiv.py" in content, "investigate doesn't reference arxiv.py" def test_review_literature_has_graceful_degradation(): @@ -135,12 +149,13 @@ def test_review_literature_has_recent_papers(): def test_search_tools_reference_exists_and_complete(): - """The search-tools reference doc should cover both scripts.""" + """The search-tools reference doc should cover all five platform scripts.""" content = Path("skills/reaper/references/search-tools.md").read_text() - assert "search_arxiv.py" in content - assert "search_iacr.py" in content + for script in ("arxiv.py", "iacr.py", "semantic_scholar.py", "dblp.py", "openalex.py"): + assert script in content, f"search-tools.md missing reference to {script}" assert "Decision Tree" in content or "decision tree" in content assert "Graceful" in content or "fallback" in content.lower() + assert "Venue Resolution Protocol" in content, "search-tools.md missing the layered venue protocol" def test_evals_json_valid(): @@ -163,10 +178,9 @@ def test_readme_mentions_prerequisites(): def test_readme_lists_search_skills(): - """README should list the new search skills.""" + """README should list the unified search-paper skill.""" content = Path("README.md").read_text() - assert "search-arxiv" in content - assert "search-iacr" in content + assert "search-paper" in content # --------------------------------------------------------------------------- @@ -189,8 +203,7 @@ def test_readme_lists_search_skills(): "skills/investigate/SKILL.md", "skills/critique/SKILL.md", "skills/synthesize/SKILL.md", - "skills/search-arxiv/SKILL.md", - "skills/search-iacr/SKILL.md", + "skills/search-paper/SKILL.md", ] @@ -200,7 +213,7 @@ def test_no_relative_python_skills_invocations(): Such relative paths only resolve if the user happens to be running the agent from the repo root. After `npx skills add`, the scripts live under a per-host install dir (e.g. ~/.agents/skills/, ~/.cursor/skills/), so - skills must use the {{SEARCH_*_SKILL_DIR}} placeholders that the agent + skills must use the {{*_SKILL_DIR}} placeholders that the agent substitutes at execution time. """ pattern = re.compile(r"python\s+skills/") @@ -217,9 +230,9 @@ def test_no_relative_python_skills_invocations(): assert not offenders, ( "Found relative `python skills/...` invocations — these break under " "npx skills install (scripts live in per-host install dirs, not " - "under skills/). Use the {{SEARCH_ARXIV_SKILL_DIR}} / " - "{{SEARCH_IACR_SKILL_DIR}} placeholders instead. Offenders: " - + ", ".join(offenders) + "under skills/). Use the {{REAPER_SKILL_DIR}} / " + "{{SEARCH_PAPER_SKILL_DIR}} / {{SKILL_DIR}} placeholders instead. " + "Offenders: " + ", ".join(offenders) ) @@ -233,9 +246,8 @@ def test_skill_dir_placeholders_are_defined(): standardized preamble vocabulary). Matches both the multi-skill form ({{REAPER_SKILL_DIR}}, - {{SEARCH_ARXIV_SKILL_DIR}}, {{SEARCH_IACR_SKILL_DIR}}) and the - own-directory form ({{SKILL_DIR}}) used by leaf skills like - search-arxiv and search-iacr. + {{SEARCH_PAPER_SKILL_DIR}}) and the own-directory form ({{SKILL_DIR}}) + used by leaf skills like /search-paper. """ placeholder_pattern = re.compile(r"\{\{([A-Z_]*SKILL_DIR)\}\}") # Words that appear in a definition paragraph (per the standardized