Skip to content
forge-ciPublic

About

One cached, batched, budget-aware gateway for gh / GitHub API reads

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

30 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ghd

A per-user daemon in front of the GitHub API, so agents that loop on gh stop burning the shared GraphQL budget.

CI License: MIT

ghd demo: ten concurrent gh pr view reads served from cache, batched GraphQL and free ETag revalidation

▶ Play the interactive demo: a real recorded session against GitHub, with chapters and the numbers it measured.

The problem

Every gh pr view, gh pr checks and gh api graphql call spends points from one 5,000-point/hour GraphQL budget per user. A handful of agents polling PRs in loops drain it, and then everyone sees GraphQL: API rate limit already exceeded for user ID ….

Before / after

gh ghd
Command gh pr view 2138 --json state,title ghd pr view 2138 --json state,title
Output gh's JSON the same fields and values
Fresh read latency ~480 ms ~5 ms
10 concurrent reads (5 PRs × 2), cold ~10 points 2 points
The same 10 reads, warm ~10 points 0 points

Numbers come from the demo and the spike. In a live check on 2026-10-25, one ghd api graphql call went upstream, and 5 parallel identical calls after it were all served fresh.

Quickstart

# Run straight from git (pin a commit SHA for reproducibility):
uvx --from git+https://github.com/forge-ci/ghd@<sha> ghd --help

# Or install it as a tool:
uv tool install git+https://github.com/forge-ci/ghd
$ ghd pr view 2138 -R talos-hq/clud --json number,state,title,mergedAt
{
  "mergedAt": "2026-10-24T21:12:09Z",
  "number": 2138,
  "state": "MERGED",
  "title": "fix(gc): retry a locked registry …"
}

The daemon starts itself on first use; there is nothing to configure.

PyPI: the ghd name on PyPI is only a 0.0.0 reservation today. pip install ghd will work with the first real release.

How it saves budget

flowchart LR
    A[agents / scripts] -->|ghd ...| C[ghd CLI]
    C -->|loopback HTTP + protobuf + bearer token| D[ghd daemon]
    D --> F{fresh? <2 s}
    F -->|yes: 0 points| C
    F -->|no| E[REST ETag check]
    E -->|304: free| C
    E -->|changed| B[batched GraphQL<br/>one query per repo per 50 ms]
    B --> G[(api.github.com)]
Loading

A pr view is answered by the cheapest step that can prove the data fresh:

step cost when
fresh 0 validated in the last 2 s (GHD_FRESH_MS); concurrent identical reads wait on one per-PR lock
revalidated 0 the PR's REST ETag returns 304 Not Modified, which GitHub does not count against the rate limit
fetched ~1 GraphQL point per batch every PR of one repo requested within 50 ms goes into one aliased GraphQL query

ghd api graphql uses the same idea for arbitrary queries: identical concurrent queries share one upstream call, and an answer is reused for the fresh window, which is exactly what an agent's re-check loop hits.

Command reference

ghd gh <gh args...>                                 a drop-in gh: native when ghd
                                                    understands it, else the real gh
ghd pr view <number> [-R owner/repo] --json f1,f2   like `gh pr view --json`
ghd api graphql -f query=... [-f k=v] [-F k=v]      like `gh api graphql`
ghd stats                                           what the daemon saved
ghd daemon                                          run the daemon (auto-started)
ghd --version | --help

ghd gh (ghd#8)

  • Takes any gh command line. Shapes ghd serves natively (pr view N [-R X] --json F, api graphql -f/-F) go through the daemon. Everything else, including --jq/--template and a failed native pr view, runs the real gh with your stdin, stdout, stderr and terminal, and returns its exit code. The output and errors are gh's own.
  • The real gh is $GHD_REAL_GH when set (a launcher with a gh shim on PATH sets it), else the first gh on PATH that is not ghd. A relay that leads back into ghd fails with one line instead of looping.
  • Relayed calls are not cached yet; caching captured reads is the next step.

ghd pr view

  • Same output as gh pr view --json. Without -R, the repository comes from the origin remote, as with gh.
  • Supported --json fields so far are the plain scalars: additions, baseRefName, body, changedFiles, closed, closedAt, createdAt, deletions, headRefName, headRefOid, id, isDraft, mergeStateStatus, mergeable, mergedAt, number, state, title, updatedAt and url.

ghd api graphql

  • Takes gh's -f (string) and -F (typed, @file) fields and prints GitHub's body.
  • Queries are deduplicated and cached for the fresh window.
  • Mutations always go upstream and clear the query cache, so no read returns data from before a write.
  • A body with errors is printed and the command exits 1, as with gh.

ghd stats prints the daemon's counters as JSON: reads, served_fresh, sentinel_304_free, sentinel_200, graphql_queries, graphql_prs_fetched, graphql_points_spent, api_graphql_upstream, api_graphql_served_fresh, graphql_remaining, core_remaining and uptime_secs.

ghd daemon runs the daemon in the foreground. You rarely need it: one start lock means concurrent cold clients start exactly one daemon.

Configuration

variable effect
GHD_FRESH_MS fresh window in milliseconds (default 2000), read once at daemon start
GHD_DEBUG when set, prints which step (fresh, revalidated, fetched) answered each read
GHD_EXE the ghd executable used to start the daemon, if not the current one
GH_TOKEN / GITHUB_TOKEN the token the daemon uses; falls back to gh auth token

Use it with agents

The point of ghd is to sit under tools that call GitHub in loops. A real consumer: obs-rust/obs-studio routes every GraphQL call through

uv run --no-project --script ci/gh_graphql.py ...

which wraps ghd, and its agent PreToolUse hook bans bare gh api graphql, so no agent session can bypass the cache by accident. The same pattern works anywhere:

  1. Pin ghd with uvx --from git+https://github.com/forge-ci/ghd@<sha> ghd.
  2. Point your scripts' gh pr view --json and gh api graphql calls at it.
  3. Add a hook or lint rule that rejects the bare gh forms.
  4. Watch ghd stats to see what was saved.

Python callers can also use the bundled extension:

from ghd.bindings import pr_view
pr_view("talos-hq/clud", 2138, ["state", "mergeable"])

Architecture

crate role
crates/ghd the public amalgam: pub use re-exports only; feature daemon
crates/ghd-cli the ghd binary (bundled into the wheel by soldr)
crates/ghd-py the PyO3 _native extension (client side only)
crates/private/ghd-core wire protocol, endpoint file, loopback client, gh-compatible CLI
crates/private/ghd-daemon loopback server, cache, sentinels, GraphQL batcher
crates/private/ghd-platform the only crate with host-specific code (cfg_select! once)
  • The CLI client is std-only; it talks to the daemon over loopback HTTP with protobuf bodies and a bearer token.
  • The daemon is built on kernal-api (async engine, HTTP client and server, JSON, secure random). No crate names tokio, reqwest or hyper directly.
  • Every third-party dependency is pinned with =.
  • More: DESIGN.md (including the roadmap: checks, lists, issues, writes and watches), docs/ARCHITECTURE.md and crates/README.md.

Development

./test        # soldr cargo test --workspace, build + install the wheel, pytest
./lint        # rustfmt, clippy -D warnings, ruff
uv run --no-project --script ci/local.py precheck   # ci-lint precheck (also a hook)

CI is declared, not scripted: ci.toml (profile rust-pypi-app) is the contract, checked by ci-lint. Repo rules, gates and hooks are in CLAUDE.md.

Releases

Merge a commit to main that bumps [workspace.package].version in Cargo.toml and version in pyproject.toml. CI then builds every platform's wheel, verifies them, tags the commit and publishes to PyPI with no stored secrets. See RELEASE.md.

License

MIT

About

One cached, batched, budget-aware gateway for gh / GitHub API reads

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages