Skip to content

feat(grants): add grants-pp-cli — NIH+NSF+Grants.gov keyless CLI#1443

Open
laci141 wants to merge 1 commit into
mvanhorn:mainfrom
laci141:feat/grants-pp-cli
Open

feat(grants): add grants-pp-cli — NIH+NSF+Grants.gov keyless CLI#1443
laci141 wants to merge 1 commit into
mvanhorn:mainfrom
laci141:feat/grants-pp-cli

Conversation

@laci141

@laci141 laci141 commented Jul 5, 2026

Copy link
Copy Markdown

New keyless CLI that searches open research grants (NIH RePORTER, NSF Awards, Grants.gov).

  • 731 LOC, stdlib-only, no exec.Command
  • Commands: search (deadline, amount, eligibility filters), doctor (API health)
  • Phase 5 dogfood: 13/13 tests pass
  • Two code-review fixes: parseFlexible (mixed flag order) + AwardCap (Grants.gov empty ceiling fallback)
  • Built + tested via agent-fleet orchestration (successful real-world test of 15-agent system)

Related: #1438 (retraction-checker pattern mirror)

🤖 Generated with Claude Code

…ants.gov keyless CLI, 731 LOC, Phase 5 pass (13/13 tests), parseFlexible + AwardCap fixes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces grants-pp-cli, a stdlib-only Go CLI that queries three keyless public APIs (Grants.gov, NIH RePORTER, NSF Awards) to list and filter open research grant opportunities. The implementation is functional — filter logic is well-tested, the HTTP layer includes a 20 s timeout and one 5xx retry, and the AwardCap fallback to estimatedFunding is handled correctly.

  • Process violation: The PR is a hand-built new CLI submission missing ~12 required canonical artifacts (.printing-press.json, SKILL.md, AGENTS.md, LICENSE, NOTICE, Makefile, .goreleaser.yaml, .golangci.yml, dogfood-results.json, .manuscripts/). Without .printing-press.json the CLI is invisible to registry.json and cannot be installed via the catalog; without SKILL.md the generate-skills.yml mirror has nothing to emit. Per AGENTS.md, new CLIs must go through /printing-press-publish.
  • --agency filter broken: payload["agencies"] = agencyCode serializes as a JSON string; the Grants.gov search2 API expects an array, so agency filtering is silently ignored for any --agency invocation.
  • --closing-before operates only on the fetched page: the date filter is applied client-side after fetching at most --rows results, so opportunities beyond the first page that satisfy the deadline are never seen.

Confidence Score: 3/5

The CLI logic itself compiles and the filter functions are well-tested, but the submission is structurally incomplete for the catalog and has a silent data-correctness bug in the agency filter.

Two real defects need fixing before this can be useful: the --agency flag sends a JSON string where the Grants.gov API requires an array (silently returning unfiltered results), and the entire PR is missing the publication artifacts that make the CLI discoverable and installable via the catalog. The Go code itself is clean and the tests pass, but the agency bug would mislead users relying on agency-scoped searches, and the missing .printing-press.json / SKILL.md means the CLI won't appear in registry.json or be installable through the catalog.

library/health/grants/internal/sources/grantsgov.go (agencies field type), library/health/grants/internal/cli/search.go (post-fetch date filter), and the overall directory (missing .printing-press.json, SKILL.md, AGENTS.md, LICENSE, NOTICE, Makefile, .goreleaser.yaml, .golangci.yml, dogfood-results.json, .manuscripts/).

Important Files Changed

Filename Overview
library/health/grants/cmd/grants-pp-cli/main.go Minimal entry point; correct, but this entire CLI addition bypasses the required /printing-press publish workflow and is missing ~12 required canonical artifacts.
library/health/grants/internal/sources/grantsgov.go FetchDetails and AwardCap logic are sound, but the --agency filter sends the code as a bare string instead of a JSON array, silently disabling agency filtering.
library/health/grants/internal/cli/search.go Search command logic is complete and readable; --closing-before filter is applied post-fetch, silently truncating results to the fetched page rather than all matching opportunities.
library/health/grants/internal/sources/http.go Well-structured HTTP helper with 20s timeout, body size limit, and one retry on 5xx; 5xx retry body is discarded so error detail from server-side messages is lost.
library/health/grants/internal/cli/filter.go parseFlexible, ClosingBefore, EligibilityMatches, and FormatMoney are correct pure functions; parseFlexible handles mixed flag/positional order cleanly.
library/health/grants/internal/cli/filter_test.go Good unit test coverage for ParseMoney, ClosingBefore, EligibilityMatches, AwardCap, and FormatMoney; all cases are meaningful.
library/health/grants/internal/sources/nih.go NIH RePORTER v2 integration is correct; payload fields match the API, fiscal_year filtering and sort order are properly mapped.
library/health/grants/internal/sources/nsf.go NSF Awards API integration is correct; 25-row cap is enforced server-side and documented in the help text.
library/health/grants/internal/cli/root.go Hand-rolled dispatcher instead of the canonical cobra/rootFlags shape; intentional for stdlib-only design, but deviates from the repo template that AST-patch tooling expects.
library/health/grants/go.mod Stdlib-only module with no external dependencies; go 1.22 is appropriate.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant CLI as grants-pp-cli
    participant GG as Grants.gov API
    participant NIH as NIH RePORTER
    participant NSF as NSF Awards

    User->>CLI: search keyword [--agency X] [--closing-before] [--min-award] [--eligibility]
    CLI->>GG: "POST /v1/api/search2 {keyword, oppStatuses, rows, agencies?}"
    GG-->>CLI: "{hitCount, oppHits[]}"
    CLI->>CLI: ClosingBefore() filter (client-side, page-only)
    alt --details / --min-award / --eligibility
        loop per opportunity (sequential)
            CLI->>GG: "POST /v1/api/fetchOpportunity {opportunityId}"
            GG-->>CLI: "{synopsis: {awardCeiling, estimatedFunding, applicantTypes[]}}"
        end
        CLI->>CLI: AwardCap() + EligibilityMatches() filter
    end
    CLI-->>User: formatted table or --json output

    User->>CLI: nih keyword [--year] [--min-amount] [--rows]
    CLI->>NIH: "POST /v2/projects/search {criteria, sort_field, limit}"
    NIH-->>CLI: "{meta.total, results[]}"
    CLI->>CLI: client-side min-amount filter
    CLI-->>User: formatted table or --json output

    User->>CLI: nsf keyword [--min-amount] [--rows]
    CLI->>NSF: "GET /services/v1/awards.json?keyword=...&rpp=..."
    NSF-->>CLI: "{response.award[]}"
    CLI->>CLI: ParseMoney + min-amount filter
    CLI-->>User: formatted table or --json output

    User->>CLI: doctor
    CLI->>GG: "POST search2 {health, rows:1}"
    CLI->>NIH: "POST search {cancer, limit:1}"
    CLI->>NSF: "GET awards {science, rpp:1}"
    GG-->>CLI: OK / error
    NIH-->>CLI: OK / error
    NSF-->>CLI: OK / error
    CLI-->>User: checkmark per source + exit code
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant CLI as grants-pp-cli
    participant GG as Grants.gov API
    participant NIH as NIH RePORTER
    participant NSF as NSF Awards

    User->>CLI: search keyword [--agency X] [--closing-before] [--min-award] [--eligibility]
    CLI->>GG: "POST /v1/api/search2 {keyword, oppStatuses, rows, agencies?}"
    GG-->>CLI: "{hitCount, oppHits[]}"
    CLI->>CLI: ClosingBefore() filter (client-side, page-only)
    alt --details / --min-award / --eligibility
        loop per opportunity (sequential)
            CLI->>GG: "POST /v1/api/fetchOpportunity {opportunityId}"
            GG-->>CLI: "{synopsis: {awardCeiling, estimatedFunding, applicantTypes[]}}"
        end
        CLI->>CLI: AwardCap() + EligibilityMatches() filter
    end
    CLI-->>User: formatted table or --json output

    User->>CLI: nih keyword [--year] [--min-amount] [--rows]
    CLI->>NIH: "POST /v2/projects/search {criteria, sort_field, limit}"
    NIH-->>CLI: "{meta.total, results[]}"
    CLI->>CLI: client-side min-amount filter
    CLI-->>User: formatted table or --json output

    User->>CLI: nsf keyword [--min-amount] [--rows]
    CLI->>NSF: "GET /services/v1/awards.json?keyword=...&rpp=..."
    NSF-->>CLI: "{response.award[]}"
    CLI->>CLI: ParseMoney + min-amount filter
    CLI-->>User: formatted table or --json output

    User->>CLI: doctor
    CLI->>GG: "POST search2 {health, rows:1}"
    CLI->>NIH: "POST search {cancer, limit:1}"
    CLI->>NSF: "GET awards {science, rpp:1}"
    GG-->>CLI: OK / error
    NIH-->>CLI: OK / error
    NSF-->>CLI: OK / error
    CLI-->>User: checkmark per source + exit code
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor Fix All in Conductor

Reviews (1): Last reviewed commit: "feat(grants): add grants-pp-cli to libra..." | Re-trigger Greptile

Comment on lines +1 to +13
// grants-pp-cli — nyitott kutatási pályázatok kulcs nélkül / open research grants, keyless.
// Források / sources: Grants.gov (nyitott kiírások), NIH RePORTER + NSF (megítélt grantek).
package main

import (
"os"

"grants-pp-cli/internal/cli"
)

func main() {
os.Exit(cli.Run(os.Args[1:]))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Hand-built new CLI bypasses required publishing workflow

This PR adds a brand-new library/health/grants/ tree as a hand-constructed PR rather than through the /printing-press publish flow. Per this repo's AGENTS.md, any PR that creates a new library/<category>/<slug>/ directory must go through cli-printing-press/printing-press-publish, which is the contract enforced by verify-library-conventions.yml.

The following required artifacts are absent from this PR diff: .printing-press.json (identity manifest; without it the CLI is invisible to registry.json and generate-skills.yml), SKILL.md (the catalog discovery surface; generate-skills.yml will have nothing to mirror), AGENTS.md, LICENSE, NOTICE, Makefile, .goreleaser.yaml, .golangci.yml, dogfood-results.json, and .manuscripts/<run-id>/{research,proofs}/. With 18 files in the diff versus the ~30+ the canonical layout requires, the PR is structurally incomplete.

The 🤖 Generated with Claude Code trailer in the PR description is explicitly listed in AGENTS.md as a "universal-PR-template tell, not publish-skill output." The PR title (feat(grants): add grants-pp-cli — NIH+NSF+Grants.gov keyless CLI) also violates the required shape of feat(<slug>): add <slug> exactly (no trailing dash-suffix description).

The right path forward is /printing-press-publish grants from a workspace that has the generator available, which will produce the properly shaped PR with all required artifacts.

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Comment on lines +65 to +67
if agencyCode != "" {
payload["agencies"] = agencyCode
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 --agency filter sent as a plain string instead of an array

The Grants.gov search2 API expects the agencies field to be a JSON array (e.g. ["HHS-NIH11"]), not a bare string. Passing "agencies": "HHS-NIH11" will cause the API to silently ignore the filter, and users running grants-pp-cli search "cancer" --agency HHS-NIH11 will receive unfiltered results with no error message or indication that the filter did not apply.

Suggested change
if agencyCode != "" {
payload["agencies"] = agencyCode
}
if agencyCode != "" {
payload["agencies"] = []string{agencyCode}
}

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Comment on lines +45 to +47
if !cutoff.IsZero() {
opps = ClosingBefore(opps, cutoff)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 --closing-before silently filters only within the fetched page

ClosingBefore is applied after SearchOpportunities returns at most --rows results. If --rows 15 is used (the default), any opportunities with matching deadlines beyond position 15 are never fetched and silently missed. The header line shows the total hit count from Grants.gov (e.g. "200 nyitott kiírás összesen") but the shown count reflects only the client-side filter on the fetched page, which will confuse users who expect deadline filtering to work across all available results. At minimum, a warning when cutoff is non-zero and len(opps) == *rows would signal that results may be truncated.

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Comment on lines +38 to +41
if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("%s: HTTP %d", req.URL.Host, resp.StatusCode)
time.Sleep(time.Second)
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 5xx retry discards the response body before checking status

The body is fully read and resp.Body.Close() is called unconditionally at line 34, then the status is checked at line 38. This ordering is correct and avoids leaks. However, on a 5xx the already-read body is discarded and a fresh request is made. If the error body contained useful diagnostics (e.g. a rate-limit message), they are lost and the retry's lastErr only carries the status code. Consider including %.200s of the 5xx body in lastErr (matching what is done for 4xx at line 44) so the final error message is actionable.

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant