Grant research agent with cited sources and outlines
Python agent that finds open grants for a nonprofit, extracts deadlines and eligibility, and drafts a cited application outline in the funder's format.
pythonanthropicpydantichttpxpytestuv
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add grant-research-agent-with-cited-sources-and-outlines-c487c3CLAUDE.md# the app
the app is a Python agent that takes a nonprofit's mission and budget and returns ranked grant matches plus a cited application outline in the funder's format. Every deadline, amount, and eligibility claim must trace to a fetched source page.
@REVIEW.md
## Stack
- Python 3.12, managed with uv
- pydantic v2 models
- httpx for fetching
- Anthropic SDK for structured extraction and drafting
- pytest, ruff, mypy
## Commands
- Install: `uv sync`
- Tests: `uv run pytest -q`
- Lint and format check: `uv run ruff check . && uv run ruff format --check .`
- Types: `uv run mypy src`
- Live run: `uv run python -m the app run --profile examples/profile.json`
- Offline run: `uv run python -m the app run --profile examples/profile.json --offline`
- Citation check: `uv run python -m the app check-citations out/report.json`
## Layout
- `src/the app/profile.py`: organization profile loading and validation
- `src/the app/search/`: Source interface and implementations
- `src/the app/extract.py`: structured extraction of deadline, amount, eligibility
- `src/the app/match.py`: eligibility, budget fit, mission scoring, expiry filter
- `src/the app/outline.py`: ApplicationOutline drafting
- `src/the app/cite.py`: Citation model and citation checks
- `src/the app/cli.py`: `run` and `check-citations` entry points
- `tests/`: pytest suite, uses fake LLM client
- `fixtures/`: saved grant pages and golden JSON, do not edit goldens without asking
- `examples/`: sample profiles
## Citation rules
- Every claim about a deadline, amount, or eligibility rule carries a Citation: url, retrieved_at, quoted snippet.
- No citation means the field is null and flagged `unverified`. Never guess.
- Never invent grants. Only report grants found in fetched pages.
- Quoted snippets must appear in the cached source text.
- Store deadlines as ISO dates with timezone when the page states one. Flag ambiguous dates for human review.
- Outlines follow the funder's section headings and word limits when given. Requirements not found in the guidelines go under "Needs confirmation".
## Fetching and secrets
- Respect robots.txt. Rate limit to 1 request per second per host.
- Cache fetched HTML under `.cache/`. It is gitignored and must never be committed.
- Live runs need `ANTHROPIC_API_KEY` from the environment. Never write keys to files or logs.
- Tests make no network or API calls. Use saved fixtures and the fake LLM client.
## Gotchas
- Funder pages change. Golden tests on fixtures will not catch live drift; use the live smoke run in Phase 5.
- Expired grants are excluded by comparing extracted deadlines to today's date in UTC-aware time.
- The `/goal` evaluator is a small fast model that sees only the conversation. Show command output in the conversation.
## Workflow
- Explore, then plan, then code, then run the verify skill before saying a task is done.
- Work phase by phase from PLAN.md. Run `/clear` and commit between phases, asking before each commit.
- Do not add dependencies beyond httpx, pydantic, anthropic, pytest, ruff, mypy without asking.
- Keep modules under 200 lines.
## Models and subagents
- Default model for main work. Effort stays at the default unless a step needs more.
- Use `ultrathink` only for outline structure design.
- `source-reader` (Haiku) reads long saved pages and returns quoted facts.
- `test-runner` (Haiku) runs the suite and returns only failures.
- Use `/effort` mid-task rather than switching models, to keep the prompt cache.
.claude/agents/source-reader.md---
name: source-reader
description: Reads a saved grant page from fixtures/ or .cache/ and returns the deadline, amount, and eligibility facts with exact quotes and line numbers. Use when the main agent needs facts from a long funder page without loading it.
model: haiku
tools: Read, Grep
disallowedTools: Edit, Write, Bash
maxTurns: 10
---
You read one saved grant page and return facts with exact quotes. You are read-only.
Input: a path under `fixtures/` or `.cache/`.
Find these fields:
- Deadline: the closing date and time, with timezone if stated.
- Amount: minimum, maximum, and total funds available, with currency.
- Eligibility: each stated criterion, one per item.
- Funder name and the page's stated URL, if present.
For each field, return:
- The value as written in the page.
- The exact quoted snippet, copied character for character.
- The line number or range where the snippet appears.
If a field is not in the page, write "not found". Do not infer, convert, or fill gaps from general knowledge. If a date is ambiguous, quote it and write "ambiguous" with no interpretation.
Keep the response under 40 lines. Do not summarize the page beyond these fields.
.claude/agents/test-runner.md---
name: test-runner
description: Runs pytest, ruff, and mypy for the the app project and returns only the failures with short tracebacks. Use after code changes, before the main agent declares a task done.
model: haiku
tools: Bash, Read
disallowedTools: Edit, Write
maxTurns: 8
---
You run the project's checks and report only what failed. You do not fix code.
Run these commands from the repo root, in order:
1. `uv run pytest -q`
2. `uv run ruff check .`
3. `uv run ruff format --check .`
4. `uv run mypy src`
Run each command once. Do not rerun unless the first run failed to start.
For each failure, report:
- The step and the test id or file:line.
- The shortest traceback lines that show the cause, at most 15 lines.
- A one-line guess at the cause, marked as a guess.
If every step passes, reply with one line: "All checks pass: pytest, ruff check, ruff format, mypy."
Rules:
- Do not print output from passing tests.
- Do not edit or create files.
- Do not run the CLI or any command that calls the network or an API.
- Do not read `.env` files or print secrets.
.claude/settings.json{
"permissions": {
"allow": [
"Bash(uv run pytest:*)",
"Bash(uv run ruff:*)",
"Bash(uv run mypy:*)",
"Bash(uv sync)",
"Bash(git status)",
"Bash(git diff:*)"
],
"ask": [
"Bash(git push:*)",
"Bash(git commit:*)",
"Bash(uv add:*)",
"Bash(uv run python -m the app run:*)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Bash(rm -rf:*)",
"Bash(curl:*)",
"Bash(wget:*)"
],
"blockWritesOutsideWorkingDirectories": true
},
"env": {
"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(python3 -c 'import sys,json; print(json.load(sys.stdin).get(\"tool_input\", {}).get(\"file_path\", \"\"))' 2>/dev/null); case \"$f\" in *.py) command -v uv >/dev/null 2>&1 && uv run ruff format \"$f\" >/dev/null 2>&1 || true ;; esac; exit 0"
}
]
}
]
}
}
.claude/skills/add-source/SKILL.md---
name: add-source
disable-model-invocation: true
argument-hint: "<source-name>"
description: Procedure for adding a new grant source to the app safely. Invoke manually with the source name; covers the Source interface, fixtures, golden extraction, rate limits, and tests.
---
# Add source
Adds one new grant source named `$ARGUMENTS`. If no name is given, ask for one before starting. Use lowercase with hyphens, for example `example-foundation`.
Work through the steps in order. Stop and ask the human at each checkpoint marked **Human check**.
## Before coding
**Human check:** Confirm the terms of use for the source have been reviewed. Do not continue until the human confirms.
Confirm that the source:
- Is publicly accessible without a login.
- Allows automated fetching under its terms and `robots.txt`.
- Is not a page that requires scraping behind a form, session, or paywall.
If any answer is no or unknown, stop and report it.
## Steps
1. **Implement the Source interface.** Create `src/the app/search/$ARGUMENTS.py`. Implement the existing `Source` interface from `src/the app/search/`. Do not change the interface itself. If the interface cannot support this source, stop and ask.
2. **Respect robots.txt and rate limits.** Fetching must go through the shared fetcher in `src/the app/`. It must check `robots.txt` for every host and allow at most 1 request per second per host. Do not add a second HTTP path.
3. **Save three fixture pages.** Save three representative pages under `fixtures/$ARGUMENTS/`. Choose one open grant, one closed or expired grant, and one with unclear eligibility or deadline wording. Record the URL and retrieval date in a short `README.md` in that folder.
4. **Write golden extraction JSON.** For each fixture, write the expected extraction under `fixtures/$ARGUMENTS/golden/`. Every extracted field must include a Citation with a quote copied exactly from the fixture. Write the golden files by hand from the fixture text. Do not generate them with the model and accept the output.
5. **Write the extraction prompt.** The prompt must require the model to quote the source text for each field. A field with no quote must be returned as null with `unverified=true`. The prompt must not allow the model to fill gaps from memory.
6. **Add tests.** Add tests under `tests/` that load the fixtures and compare extraction to the golden JSON. Tests must not make network calls. Use the fake LLM client for model responses.
7. **Run verify.** Run the `verify` skill. All steps must pass or be skipped for a stated reason.
## Checklist
Confirm each item before reporting done:
- [ ] Terms of use were reviewed by the human.
- [ ] The source does not scrape login-gated pages.
- [ ] `robots.txt` is checked for every host.
- [ ] Rate limit is 1 request per second per host.
- [ ] Three fixtures are saved with a README recording URL and date.
- [ ] Golden JSON was written by hand from fixture text.
- [ ] Every extracted field has a Citation whose quote appears in the fixture.
- [ ] The extraction prompt quotes source text for each field.
- [ ] Tests run with no network access.
- [ ] The `verify` skill passes.
## Rules
- Never edit an existing golden file to match new extraction output without asking the human first.
- Never commit anything in `.cache/`.
- Do not add dependencies beyond httpx, pydantic, and anthropic without asking.
- Report the source name, files created, and the verify result when done.
.claude/skills/outline-format/SKILL.md---
name: outline-format
description: Guidance for mapping a funder's guidelines to a the app ApplicationOutline. Use when drafting or testing outline output, parsing guideline sections, or checking outline headings against fixtures.
---
# Outline format
Use this skill when you draft or test an `ApplicationOutline`. The outline is a structure for the human writer. It is not final prose.
## Rules
1. **Preserve funder names and order.** Section headings must match the funder's guideline headings in both wording and sequence. Do not rename, merge, or reorder them to fit the organization profile.
2. **Carry limits.** Record every word limit, page limit, character limit, and font or format rule that the guidelines state. Attach each limit to the section it applies to. Cite the guideline snippet that states it.
3. **List required attachments.** Collect every attachment the guidelines require, such as budget template, board list, or 501(c)(3) letter. List each one with a citation. Do not infer attachments that the guidelines do not name.
4. **Add prompts per section.** Under each section, add bullet prompts that help the writer draft. Draw prompts from the organization profile, such as mission, budget range, or focus areas. A prompt is a question or a cue. It is not a claim about the funder.
5. **Cite every requirement.** Each requirement in the outline needs a citation to a guideline snippet. A requirement with no snippet must not appear as a requirement.
6. **Flag unknowns.** Anything the guidelines seem to require but do not state clearly goes under a section titled `Needs confirmation`. Do not decide these yourself.
## Procedure
1. Read the guideline pages with the `source-reader` subagent, if available. Ask it for section headings, limits, and attachments, each with a quoted snippet.
2. Build the section list in the funder's order.
3. For each section, attach limits and prompts.
4. Add attachments as a separate list.
5. Move every unclear item to `Needs confirmation`.
6. Check the result against the fixture headings for this funder. They must match exactly.
## Output example
```markdown
# Application outline: Acme, 2026 cycle
## 1. Organization overview
- Limit: 500 words[^1]
- Prompts:
- Summarize the mission in one paragraph (source: profile.mission)
- State the year founded and the service area (source: profile.geography)
## 2. Program description
- Limit: 1,500 words, 3 pages maximum[^2]
- Prompts:
- Describe the program that the grant would fund
- Give the target population size and how it was counted
## 3. Budget
- Attachment: Budget template (Form B)[^3]
- Prompts:
- Show the request amount within the stated range (source: profile.budget)
## Required attachments
- Form B, budget template[^3]
- Board roster[^4]
## Needs confirmation
- Whether section 2 counts toward the page limit when charts are included
- Whether the board roster must be signed
[^1]: "Organization overview: no more than 500 words." Guidelines, https://example.com retrieved 2026-11-01.
[^2]: "Program description, 1,500 words or three pages." Guidelines, same URL.
[^3]: "Complete Form B, Budget." Guidelines, same URL.
[^4]: "Attach the current board roster." Guidelines, same URL.
```
## Checks before reporting
- Headings match the funder's headings exactly, in order.
- Every limit and attachment has a footnote with a quoted snippet.
- Every requirement has a citation.
- Unknowns are under `Needs confirmation`, not stated as facts.
- No requirement is invented from memory.
.claude/skills/verify/SKILL.md---
name: verify
description: Run before declaring any the app task done. Executes lint, format, type, test, smoke, and citation checks in order and reports pass or fail per step.
allowed-tools: Bash(uv run:*)
---
# Verify
Use this skill before saying any task is done. Run the steps in order. Stop at the first failing step, report it, and do not continue to later steps.
## Steps
Run each command from the repository root.
1. Lint: `uv run ruff check .`
2. Format check: `uv run ruff format --check .`
3. Types: `uv run mypy src`
4. Tests: `uv run pytest -q`
5. Offline smoke run: `uv run python -m the app run --profile examples/profile.json --offline`
6. Citation check: `uv run python -m the app check-citations out/report.json`
## Conditions for skipped steps
- Step 5 is skipped until the `run` command exists and supports `--offline`. Phase 1 may only stub it. Note the skip in the report as "skipped: CLI not implemented yet". Do not treat a stub as a pass.
- Step 6 is skipped until `out/report.json` exists. Note the skip as "skipped: no report yet". Once the report exists, step 6 is mandatory.
## Report format
Print one line per step:
```
1. ruff check . PASS
2. ruff format --check . PASS
3. mypy src FAIL
4. pytest -q not run
5. offline smoke not run
6. citation check not run
```
For the first failing step, print its output trimmed to the last 30 lines. Do not paste full logs. Then state the most likely cause in one sentence.
Finish with one of these lines:
- `VERIFY: PASS` when every step passed or was skipped for a stated reason.
- `VERIFY: FAIL at step N` otherwise.
## Rules
- Never edit files under `fixtures/` or any golden JSON file to make a check pass. If a golden file looks wrong, stop and ask the user.
- Never weaken a check. Do not add `# type: ignore`, `# noqa`, or skip markers to get a pass. Fix the code or report the failure.
- Do not run the live CLI with a real API key. The smoke step must use `--offline`.
- If a command is missing (for example `uv` is not installed), report it as a failure. Do not guess a substitute.
- Do not print secrets or environment variables.
INSTALL.md# Install the app Claude Code setup
## Prerequisites
- Claude Code 2.1.296 or newer (check with `claude --version`)
- Python 3.12
- uv
- git
## Copy files
1. Copy the generated files into your repo root. Keep the `.claude/` paths as given.
2. If there is no pyproject.toml, run `uv init`.
3. Add the dependencies:
- `uv add httpx pydantic anthropic`
- `uv add --dev pytest ruff mypy`
4. Add `.cache/`, `out/`, and `.env` to `.gitignore`.
## Install plugins
Run these inside Claude Code:
- `/plugin install code-review@claude-plugins-official`
- `/plugin install feature-dev@claude-plugins-official`
- `/plugin install commit-commands@claude-plugins-official`
- `/plugin install security-guidance@claude-plugins-official`
## Set the API key
Export the key in your shell for live runs only:
`export ANTHROPIC_API_KEY=YOUR_API_KEY`
Never write the key to a file or commit it. Tests and offline runs do not need it.
## Start Claude Code
1. Run `claude` in the repo.
2. Run `/doctor` to confirm settings load.
3. Run `/init` only if CLAUDE.md is missing.
4. Paste the Phase 1 kickoff prompt from this plan to begin.
## Working habits
- Run `/clear` between phases.
- Run `/compact` at natural breaks.
- Run `/usage` to check where tokens went.
- `/goal` uses the small fast model as its evaluator. It judges only what appears in the conversation, so the check command output must be printed there.
PLAN.md# Build plan
Five phases. Each ends with a `/goal` prompt. Run `/clear` and commit between phases (ask before committing).
Before any phase, read CLAUDE.md and SPEC.md. Explore, then plan, then code, then verify.
## Phase 1: Skeleton and models
Build the uv project, pydantic models (`OrganizationProfile`, `Citation`, `GrantMatch`, `ApplicationOutline`), the CLI stub, the fake LLM client, and a working verify skill.
/goal Models and CLI stub exist under src/the app. Done when `uv run pytest -q`, `uv run ruff check .`, and `uv run mypy src` all pass and their output is printed in this conversation. Constraints: no network or API calls in tests, no secrets in files, no new dependencies beyond httpx, pydantic, anthropic, pytest, ruff, mypy. Stop after 25 turns and report what remains.
## Phase 2: Fetch and extract
Add the `Source` interface, an httpx fetcher with cache under .cache/, robots.txt checks, a 1 request per second limit per host, and extraction of deadline, amount, and eligibility with citations from fixtures.
/goal Golden-file tests pass for 5 fixtures in fixtures/, and every extracted field is either cited or flagged unverified. Done when `uv run pytest -q` passes and its output is printed in this conversation. Constraints: no network in tests, no API calls in tests, do not edit golden files without asking, no secrets in files. Stop after 40 turns and report what remains.
## Phase 3: Match and rank
Add eligibility pass, fail, or unknown; budget fit; mission similarity scoring; and expired grant filtering.
/goal Tests cover expired, ineligible, and unknown cases, and they pass. Done when `uv run pytest -q` passes and its output is printed in this conversation. Constraints: no network in tests, dates handled timezone-aware, no edits to golden files without asking. Stop after 30 turns and report what remains.
## Phase 4: Outline drafting
Parse funder guidelines into sections and limits, then draft an outline with citations. The model must not invent requirements.
/goal An outline test asserts headings equal fixture headings, and every requirement in the outline cites a guideline snippet found in the cached source text. Done when `uv run pytest -q` passes and its output is printed in this conversation. Constraints: no network in tests, unknown requirements go under "Needs confirmation", no edits to golden files without asking. Stop after 35 turns and report what remains.
## Phase 5: Report and hardening
Add the markdown and JSON report, error handling, a citation check that rejects uncited claims, and one live smoke run documented in the README.
/goal `uv run python -m the app run --profile examples/profile.json --offline` writes out/report.json and a markdown report, `uv run python -m the app check-citations out/report.json` passes, and all verify steps pass. Done when the full verify skill output is printed in this conversation. Constraints: no network in tests, no secrets in files or logs, do not commit .cache/ or out/. Stop after 30 turns and report what remains.
## Notes on /goal
- The evaluator is the small fast model. It runs no tools and judges only what appears in the conversation, so every check command's output must be shown.
- A `/goal` condition has a measurable end state, an exact check, constraints, and a turn bound.
- Use `/clear` between phases to keep context small.
REVIEW.md# Review rules
Use these rules for /code-review and the reviewer subagent. Check every diff against them.
## Citations and facts
- Reject any output field that asserts a deadline, amount, or eligibility fact without a Citation.
- Check that each citation snippet appears verbatim in the cached source text for its url.
- Flag any prompt that lets the model fill gaps from memory instead of the fetched page.
- Unverifiable fields must be null and marked unverified, never guessed.
## Tests and secrets
- Flag network or API calls inside tests. Tests use fixtures/ and the fake LLM client.
- Flag hard-coded keys, any logging of ANTHROPIC_API_KEY, and committed .cache/ or out/ contents.
- Flag edits to golden fixtures made to get tests passing.
## Fetching
- Check rate limiting (1 request per second per host) on every new fetcher.
- Check robots.txt handling on every new fetcher.
- Flag fetchers that skip the cache or log full response bodies.
## Dates and expiry
- Check that dates are timezone-aware when a timezone is stated.
- Flag ambiguous dates that are not marked for human review.
- Check that expired grants are excluded before ranking.
## Code style
- Prefer small functions and typed pydantic models over loose dicts.
- Flag modules over 200 lines.
## Findings format
Report each finding as:
- severity: blocker, major, or minor
- file:line
- issue: one sentence
- suggested fix: one sentence
SPEC.md# Grant research agent spec
## Problem and user
Solo or small-team grant writers at nonprofits need a vetted shortlist of open grants and a head start on application drafts. They cannot afford to chase expired programs or invented eligibility rules.
## Inputs
`OrganizationProfile`:
- `name`
- `mission` (free text)
- `budget` (annual operating budget range, not detailed financials)
- `geography` (list of regions or countries)
- `focus_areas` (list of strings)
- `org_type` (for example 501(c)(3), charity, cooperative)
- `past_funders` (optional list)
## Outputs
`GrantMatch`:
- `title`, `funder`, `url`
- `fit_score` (0 to 1)
- `fit_reasons` (list)
- `amount_range`, `deadline`, each wrapped with a `Citation` or set to `None` with `unverified=True`
- `eligibility` (list of criteria, each with a status and a `Citation`)
`ApplicationOutline`:
- `sections` in the funder's order and names
- bullet prompts per section, drawn from the organization profile
- word and page limits, required attachments
- citations to funder guidelines
- a "Needs confirmation" list for requirements without a source
`Citation`: `url`, `retrieved_at` (timezone-aware), `quote` (exact snippet from the cached page).
## Pipeline
1. Profile normalization and validation.
2. Source search through the `Source` interface.
3. Page fetch with cache, robots.txt check, and rate limit.
4. Structured extraction of deadline, amount, and eligibility, each with a citation.
5. Eligibility and budget matching (pass, fail, or unknown per criterion).
6. Ranking by fit score.
7. Outline drafting for the top matches.
8. Markdown and JSON report.
## Source strategy
- A pluggable `Source` interface comes first.
- The first implementation reads a curated list of funder URLs and the grants.gov search API.
- Other sources are added later with the `add-source` procedure.
## Filtering rules
- Closed or expired grants are excluded. Deadlines are compared to today's date in the stated timezone.
- Ambiguous dates are flagged for human review, not resolved by guessing.
- Eligibility checks return pass, fail, or unknown per criterion, each with a citation or an explicit unknown.
## Non-goals
- Submitting applications.
- Writing final prose for the applicant.
- Handling confidential financials beyond a budget range.
- Inventing grants or requirements not found in fetched pages.
## Acceptance criteria
- On fixtures, extraction matches golden JSON files exactly.
- Every output field has a citation or is flagged `unverified`.
- Outline headings match the funder's headings in text and order.
- Expired grants never appear in the ranked list.
- The CLI exits nonzero when `ANTHROPIC_API_KEY` is missing in non-offline mode.
- `pytest` makes no network or API calls.
## Open questions
- Which sources to add after grants.gov.
- How to score fit: keyword overlap, embeddings, or model judgment with citations.
- How to handle PDF guidelines (text extraction, OCR, or manual upload).