prhub 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prhub-0.1.0/.gitignore +8 -0
- prhub-0.1.0/CHANGELOG.md +27 -0
- prhub-0.1.0/LICENSE +21 -0
- prhub-0.1.0/PKG-INFO +250 -0
- prhub-0.1.0/README.md +221 -0
- prhub-0.1.0/prhub/__init__.py +2 -0
- prhub-0.1.0/prhub/__main__.py +5 -0
- prhub-0.1.0/prhub/cli.py +426 -0
- prhub-0.1.0/prhub/commits.py +207 -0
- prhub-0.1.0/prhub/conditions.py +86 -0
- prhub-0.1.0/prhub/daemon.py +213 -0
- prhub-0.1.0/prhub/github.py +164 -0
- prhub-0.1.0/prhub/refs.py +94 -0
- prhub-0.1.0/prhub/store.py +166 -0
- prhub-0.1.0/pyproject.toml +64 -0
- prhub-0.1.0/tests/test_cli.py +103 -0
- prhub-0.1.0/tests/test_commits.py +124 -0
- prhub-0.1.0/tests/test_core.py +177 -0
prhub-0.1.0/.gitignore
ADDED
prhub-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to prhub are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/),
|
|
4
|
+
and versions follow [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-10-11
|
|
9
|
+
|
|
10
|
+
First public release.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `prhub daemon`: one poller per machine. One batched GraphQL request a minute reads every tracked PR and watched
|
|
15
|
+
commit; a discovery search finds your open PRs every few minutes. It slows down when the GitHub budget runs low
|
|
16
|
+
and waits for the reset when rate-limited.
|
|
17
|
+
- `prhub wait` for PRs with `done`, `green`, `checks`, `merged`, `closed`, `review` and `change`. `done` requires an
|
|
18
|
+
APPROVED review of the current head that is newer than the head commit.
|
|
19
|
+
- `prhub wait` for commits (`owner/repo@sha`) with `checks`, `green`, `deployed` and `change`, plus `--env` and
|
|
20
|
+
`--workflow`. `deployed` reads GitHub Deployments and `deployed/<env>` statuses, and counts a later deploy that
|
|
21
|
+
contains the commit.
|
|
22
|
+
- `prhub state`, `prhub list` (`--ready`, `--all`, `--json`), `prhub watch` / `unwatch` and `prhub status`, which
|
|
23
|
+
says `PAUSED` during a GitHub rate limit.
|
|
24
|
+
- `prhub hook claude`: a Claude Code `PreToolUse` hook that refuses GitHub polling loops and points to `prhub wait`.
|
|
25
|
+
- `prhub install-launchd` (macOS) and `prhub install-systemd` (Linux) to run the daemon as a service.
|
|
26
|
+
- Names for PRs and commits: URLs, `owner/repo#N`, `owner/repo@sha`, nicknames from the config and local clones,
|
|
27
|
+
and bare numbers or `@sha` inside a clone.
|
prhub-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Naves
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
prhub-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prhub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One GitHub poller for many coding agents: they wait on local PR and deploy state instead of each polling the API
|
|
5
|
+
Project-URL: Homepage, https://github.com/danielnc/prhub
|
|
6
|
+
Project-URL: Documentation, https://github.com/danielnc/prhub#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/danielnc/prhub
|
|
8
|
+
Project-URL: Issues, https://github.com/danielnc/prhub/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/danielnc/prhub/blob/main/CHANGELOG.md
|
|
10
|
+
Author: prhub contributors
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ci,claude-code,cli,codex,coding-agents,deployments,github,pull-requests,rate-limit
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
26
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# prhub
|
|
31
|
+
|
|
32
|
+
**One GitHub poller for many coding agents.**
|
|
33
|
+
|
|
34
|
+
[](https://github.com/danielnc/prhub/actions/workflows/ci.yml)
|
|
35
|
+
[](https://scorecard.dev/viewer/?uri=github.com/danielnc/prhub)
|
|
36
|
+
[](https://pypi.org/project/prhub/)
|
|
37
|
+
[](https://pypi.org/project/prhub/)
|
|
38
|
+
[](https://codecov.io/gh/danielnc/prhub)
|
|
39
|
+
[](https://coderabbit.ai)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
Coding agents (Claude Code, Codex, Cursor, …) spend a lot of their time waiting: for CI on the PR they just pushed,
|
|
43
|
+
for a review, for a merge, for the deploy. Left alone, each one waits the same way, with its own polling loop:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
gh pr checks 2814 --watch
|
|
47
|
+
until gh pr view 2814 --json state | grep -q MERGED; do sleep 30; done
|
|
48
|
+
gh run watch 123456
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Every one of those loops spends the same budget: the **5,000 requests per hour** of the one GitHub token they all
|
|
52
|
+
share. With a dozen agents the budget is gone within the hour, and then *every* `gh` command fails for everyone,
|
|
53
|
+
you included, until it resets. On the author's machine, a single agent's monitoring loop once made about 190
|
|
54
|
+
GraphQL calls a minute and emptied the shared budget in under half an hour.
|
|
55
|
+
|
|
56
|
+
prhub replaces all of those loops with **one daemon per machine**. Once a minute it reads every PR and commit
|
|
57
|
+
anyone is waiting on in a single batched GraphQL request, and writes the state to local files. Agents block on
|
|
58
|
+
those files with `prhub wait`, which never calls GitHub.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
prhub wait api#2814 --until done # open, approved at the current head, CI green, mergeable
|
|
62
|
+
prhub wait api#2814 --until checks --timeout 1800 # CI finished, whatever the result
|
|
63
|
+
prhub wait acme/api@3a1e48e --until deployed # this commit is live in production
|
|
64
|
+
prhub state https://github.com/acme/api/pull/42 # current state, instantly
|
|
65
|
+
prhub list --ready # your PRs that are ready to merge, right now
|
|
66
|
+
prhub status # daemon health and the GitHub budget left
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
┌──────────────── one machine ────────────────┐
|
|
73
|
+
GitHub API ◄── │ prhub daemon ──► ~/.local/state/prhub/*.json │ ◄── prhub wait / state / list
|
|
74
|
+
(1 batched │ every 60 s │ (any number of agents,
|
|
75
|
+
query/min) └──────────────────────────────────────────────┘ no API calls)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **Discovery.** Every few minutes the daemon runs one search for your open PRs (`is:pr is:open author:@me`, or
|
|
79
|
+
your own query), so agents don't have to register anything.
|
|
80
|
+
- **Polling.** Every minute, one GraphQL request reads every tracked PR (40 per request) and every watched commit,
|
|
81
|
+
together with each repo's recent deployments. With 30 to 40 open PRs that costs about 1 point per poll on the
|
|
82
|
+
author's machine, roughly 60 to 120 points an hour, however many agents are waiting.
|
|
83
|
+
- **Waiting.** `prhub wait` tracks whatever it is asked about, nudges the daemon for an early poll (at most one
|
|
84
|
+
every 15 s), then reads local files until the condition holds. Only data fetched *after* the wait started is
|
|
85
|
+
judged, so a stale file never ends a wait.
|
|
86
|
+
- **Backing off.** Below 1,000 points left the daemon slows to every 3 minutes, below 300 to every 10, and when
|
|
87
|
+
GitHub rate-limits it, it waits for the reset. During a rate limit `prhub status` says `PAUSED`, and waits keep
|
|
88
|
+
waiting rather than failing, so agents don't fall back to polling by hand against the same exhausted limit.
|
|
89
|
+
|
|
90
|
+
## Install
|
|
91
|
+
|
|
92
|
+
Requires Python 3.11+ and the [GitHub CLI](https://cli.github.com), logged in (`gh auth login`). prhub reads the
|
|
93
|
+
token from `gh auth token` on every poll, never stores it, and has no other dependencies.
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
uv tool install prhub # or: pipx install prhub
|
|
97
|
+
prhub install-launchd # macOS: run the daemon at login
|
|
98
|
+
prhub install-systemd # Linux: run it as a systemd user service
|
|
99
|
+
prhub status # "daemon: healthy" within a minute
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To try it without installing a service, run `prhub daemon` in a terminal. Uninstall: on macOS,
|
|
103
|
+
`launchctl bootout gui/$(id -u)/dev.prhub.daemon` and delete `~/Library/LaunchAgents/dev.prhub.daemon.plist`; on
|
|
104
|
+
Linux, `systemctl --user disable --now prhub` and delete `~/.config/systemd/user/prhub.service`.
|
|
105
|
+
|
|
106
|
+
## Pull requests
|
|
107
|
+
|
|
108
|
+
`prhub wait <pr> --until <condition>[,<condition>…]` returns as soon as any one condition holds.
|
|
109
|
+
|
|
110
|
+
| condition | met when |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `done` | open, not a draft, CI SUCCESS, merge state CLEAN (configurable), and an APPROVED review **of the current head** that is newer than the head commit |
|
|
113
|
+
| `green` | the CI rollup is SUCCESS |
|
|
114
|
+
| `checks` | CI finished, whatever the result |
|
|
115
|
+
| `merged` | the PR was merged |
|
|
116
|
+
| `closed` | the PR was merged or closed |
|
|
117
|
+
| `review` | a new review arrived after the wait started |
|
|
118
|
+
| `change` | anything relevant changed: state, draft, CI, review, merge state, head commit |
|
|
119
|
+
|
|
120
|
+
`done` deliberately does not trust GitHub's `reviewDecision` alone: it stays APPROVED after a new push unless the
|
|
121
|
+
repository dismisses stale reviews, and a force-push with an unchanged patch relabels an old review onto the new
|
|
122
|
+
head. `prhub state` marks such a PR `review=APPROVED(stale: no approval of this head)`.
|
|
123
|
+
|
|
124
|
+
When CI goes red, `state` and `wait` also print the names of the failing checks.
|
|
125
|
+
|
|
126
|
+
## Commits: CI and deploys
|
|
127
|
+
|
|
128
|
+
The same daemon watches commits, in any repository, so agents stop polling `gh run watch` and the Actions API.
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
prhub wait acme/api@3a1e48e --until green # every CI run on the commit passed
|
|
132
|
+
prhub wait api@3a1e48e --until green --workflow deploy # only runs whose name contains "deploy"
|
|
133
|
+
prhub wait https://github.com/acme/api/commit/3a1e48e --until deployed --env production
|
|
134
|
+
prhub state @3a1e48e # a commit of the clone you are in
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| condition | met when |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `checks` | every run on the commit finished, whatever the result |
|
|
140
|
+
| `green` | every run passed (exits `3` as soon as one fails) |
|
|
141
|
+
| `deployed` | the commit is live in `--env` (default `production`, or `any`) |
|
|
142
|
+
| `change` | anything about the commit's runs or deploys changed |
|
|
143
|
+
|
|
144
|
+
It works the same whatever a repository's workflow names, default branch or release style:
|
|
145
|
+
|
|
146
|
+
- **CI** is every check suite that ran something on the commit. Suites from apps that never start a run (they sit
|
|
147
|
+
QUEUED forever) and cron-triggered runs (they attach to whatever the branch head is) are left out. A run
|
|
148
|
+
cancelled because a newer run of the same workflow replaced it is not a failure.
|
|
149
|
+
- **Deployed** reads [GitHub Deployments](https://docs.github.com/en/rest/deployments), plus commit statuses named
|
|
150
|
+
like `deployed/<env>`. A successful deploy of a *later* commit counts when that commit contains yours (asked once
|
|
151
|
+
per new deployment through the compare API), so nightly releases, batched deploys and a cancelled deploy followed
|
|
152
|
+
by a newer one all answer correctly. If a repository records no Deployments, use
|
|
153
|
+
`--until green --workflow <name of its deploy workflow>`.
|
|
154
|
+
|
|
155
|
+
A commit stays tracked for 6 hours after the last `state` or `wait` that asked about it.
|
|
156
|
+
|
|
157
|
+
## Naming things
|
|
158
|
+
|
|
159
|
+
| you type | means |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `https://github.com/acme/api/pull/42`, `acme/api#42` | a PR |
|
|
162
|
+
| `42` | PR 42 of the clone in the current directory |
|
|
163
|
+
| `api #42`, `api#42` | a PR, by repo name or nickname (see below) |
|
|
164
|
+
| `https://github.com/acme/api/commit/3a1e48e`, `acme/api@3a1e48e` | a commit (7 to 40 hex characters) |
|
|
165
|
+
| `api@3a1e48e`, `@3a1e48e` | a commit, by nickname, or in the current clone |
|
|
166
|
+
|
|
167
|
+
Nicknames come from `[aliases]` in the config, and from the names of the git clones found under `clone_roots`.
|
|
168
|
+
|
|
169
|
+
## Exit codes
|
|
170
|
+
|
|
171
|
+
`0` the condition was met · `1` timed out · `2` bad arguments, or no such commit · `3` the condition can never be
|
|
172
|
+
met (the PR closed; for a commit, a run failed under `green` or its deploy failed) · `4` the daemon is not running.
|
|
173
|
+
|
|
174
|
+
## Agent setup
|
|
175
|
+
|
|
176
|
+
**Tell your agents.** Add something like this to your agents' instructions (`CLAUDE.md`, `AGENTS.md`, …); a
|
|
177
|
+
longer version is in [docs/agents.md](docs/agents.md):
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
## Waiting on GitHub: use prhub, never poll
|
|
181
|
+
Every agent on this machine shares one GitHub token. To wait for a PR, CI or a deploy, run
|
|
182
|
+
`prhub wait <pr or owner/repo@sha> --until <condition>` (in the background if you need to keep working), and use
|
|
183
|
+
`prhub state <pr>` or `prhub list --ready` for the current state. These read local files and cost nothing.
|
|
184
|
+
Never loop over `gh pr checks`, `gh pr view`, `gh run watch` or `gh api` with `sleep`.
|
|
185
|
+
If `prhub status` says PAUSED, that is a GitHub rate limit: keep waiting, don't poll by hand.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Enforce it (Claude Code).** `prhub hook claude` is a `PreToolUse` hook that refuses `gh pr checks --watch`,
|
|
189
|
+
`gh run watch`, `watch … gh …` and shell loops that `sleep` around `gh pr`, `gh run` or `gh api`, and tells the
|
|
190
|
+
agent which `prhub wait` to use instead. One-off reads and every write pass through untouched, and the hook stands
|
|
191
|
+
aside entirely when the daemon is not healthy. Add it to `~/.claude/settings.json`:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{ "hooks": { "PreToolUse": [ { "matcher": "Bash",
|
|
195
|
+
"hooks": [ { "type": "command", "command": "prhub hook claude", "timeout": 5 } ] } ] } }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Put `PRHUB_ALLOW=1` anywhere in a command to let one loop through on purpose.
|
|
199
|
+
|
|
200
|
+
## Configuration
|
|
201
|
+
|
|
202
|
+
`~/.config/prhub/config.toml` (override the path with `PRHUB_CONFIG`). Every key is optional:
|
|
203
|
+
|
|
204
|
+
```toml
|
|
205
|
+
search = "is:pr is:open author:@me org:acme" # what the daemon discovers by itself
|
|
206
|
+
interval = 60 # seconds between polls
|
|
207
|
+
discover_every = 180 # seconds between discovery searches
|
|
208
|
+
clone_roots = ["~/code"] # clones under here become repo nicknames
|
|
209
|
+
|
|
210
|
+
[aliases]
|
|
211
|
+
api = "acme/api" # nickname -> owner/repo, or the name of a local clone
|
|
212
|
+
web = "acme/web-app"
|
|
213
|
+
|
|
214
|
+
[done]
|
|
215
|
+
merge_states = ["CLEAN", "HAS_HOOKS"] # merge states that count as mergeable for `done`
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Any PR or commit an agent waits on is tracked automatically, even if the search would not find it (someone else's
|
|
219
|
+
PR, another organization). Closed PRs drop out an hour after they close.
|
|
220
|
+
|
|
221
|
+
## Files
|
|
222
|
+
|
|
223
|
+
Everything lives in `~/.local/state/prhub` (override with `PRHUB_HOME`):
|
|
224
|
+
|
|
225
|
+
| path | what |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `prs/<owner>__<repo>__<n>.json` | one record per PR: a summary plus the raw GraphQL fields |
|
|
228
|
+
| `commits/<owner>__<repo>__<sha>.json` | one record per watched commit |
|
|
229
|
+
| `watch.json`, `commit-watch.json` | what agents asked to track, and who asked (`PRHUB_CALLER`, else a tmux or Herdr pane id) |
|
|
230
|
+
| `events.jsonl` | every state change, one JSON line each |
|
|
231
|
+
| `daemon.json`, `daemon.log` | daemon health, the GitHub budget left, and the log |
|
|
232
|
+
|
|
233
|
+
Other tools can read these too. A dashboard, a merge queue or a status bar can use them without calling GitHub.
|
|
234
|
+
|
|
235
|
+
## Limits
|
|
236
|
+
|
|
237
|
+
- github.com only for now; GitHub Enterprise Server is not supported.
|
|
238
|
+
- One daemon per machine and per GitHub token. Agents on other machines need their own daemon.
|
|
239
|
+
- The state is up to one poll interval old (60 s by default). `prhub state --fresh` asks for a poll first.
|
|
240
|
+
- `done` checks the review decision, CI and merge state that GitHub reports. If your merge rules need more (a
|
|
241
|
+
specific reviewer, resolved threads), check those before merging.
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
245
|
+
Bug reports and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md). Report security issues
|
|
246
|
+
privately, as described in [SECURITY.md](SECURITY.md).
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
[MIT](LICENSE)
|
prhub-0.1.0/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# prhub
|
|
2
|
+
|
|
3
|
+
**One GitHub poller for many coding agents.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/danielnc/prhub/actions/workflows/ci.yml)
|
|
6
|
+
[](https://scorecard.dev/viewer/?uri=github.com/danielnc/prhub)
|
|
7
|
+
[](https://pypi.org/project/prhub/)
|
|
8
|
+
[](https://pypi.org/project/prhub/)
|
|
9
|
+
[](https://codecov.io/gh/danielnc/prhub)
|
|
10
|
+
[](https://coderabbit.ai)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
|
|
13
|
+
Coding agents (Claude Code, Codex, Cursor, …) spend a lot of their time waiting: for CI on the PR they just pushed,
|
|
14
|
+
for a review, for a merge, for the deploy. Left alone, each one waits the same way, with its own polling loop:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gh pr checks 2814 --watch
|
|
18
|
+
until gh pr view 2814 --json state | grep -q MERGED; do sleep 30; done
|
|
19
|
+
gh run watch 123456
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every one of those loops spends the same budget: the **5,000 requests per hour** of the one GitHub token they all
|
|
23
|
+
share. With a dozen agents the budget is gone within the hour, and then *every* `gh` command fails for everyone,
|
|
24
|
+
you included, until it resets. On the author's machine, a single agent's monitoring loop once made about 190
|
|
25
|
+
GraphQL calls a minute and emptied the shared budget in under half an hour.
|
|
26
|
+
|
|
27
|
+
prhub replaces all of those loops with **one daemon per machine**. Once a minute it reads every PR and commit
|
|
28
|
+
anyone is waiting on in a single batched GraphQL request, and writes the state to local files. Agents block on
|
|
29
|
+
those files with `prhub wait`, which never calls GitHub.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
prhub wait api#2814 --until done # open, approved at the current head, CI green, mergeable
|
|
33
|
+
prhub wait api#2814 --until checks --timeout 1800 # CI finished, whatever the result
|
|
34
|
+
prhub wait acme/api@3a1e48e --until deployed # this commit is live in production
|
|
35
|
+
prhub state https://github.com/acme/api/pull/42 # current state, instantly
|
|
36
|
+
prhub list --ready # your PRs that are ready to merge, right now
|
|
37
|
+
prhub status # daemon health and the GitHub budget left
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌──────────────── one machine ────────────────┐
|
|
44
|
+
GitHub API ◄── │ prhub daemon ──► ~/.local/state/prhub/*.json │ ◄── prhub wait / state / list
|
|
45
|
+
(1 batched │ every 60 s │ (any number of agents,
|
|
46
|
+
query/min) └──────────────────────────────────────────────┘ no API calls)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Discovery.** Every few minutes the daemon runs one search for your open PRs (`is:pr is:open author:@me`, or
|
|
50
|
+
your own query), so agents don't have to register anything.
|
|
51
|
+
- **Polling.** Every minute, one GraphQL request reads every tracked PR (40 per request) and every watched commit,
|
|
52
|
+
together with each repo's recent deployments. With 30 to 40 open PRs that costs about 1 point per poll on the
|
|
53
|
+
author's machine, roughly 60 to 120 points an hour, however many agents are waiting.
|
|
54
|
+
- **Waiting.** `prhub wait` tracks whatever it is asked about, nudges the daemon for an early poll (at most one
|
|
55
|
+
every 15 s), then reads local files until the condition holds. Only data fetched *after* the wait started is
|
|
56
|
+
judged, so a stale file never ends a wait.
|
|
57
|
+
- **Backing off.** Below 1,000 points left the daemon slows to every 3 minutes, below 300 to every 10, and when
|
|
58
|
+
GitHub rate-limits it, it waits for the reset. During a rate limit `prhub status` says `PAUSED`, and waits keep
|
|
59
|
+
waiting rather than failing, so agents don't fall back to polling by hand against the same exhausted limit.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Requires Python 3.11+ and the [GitHub CLI](https://cli.github.com), logged in (`gh auth login`). prhub reads the
|
|
64
|
+
token from `gh auth token` on every poll, never stores it, and has no other dependencies.
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
uv tool install prhub # or: pipx install prhub
|
|
68
|
+
prhub install-launchd # macOS: run the daemon at login
|
|
69
|
+
prhub install-systemd # Linux: run it as a systemd user service
|
|
70
|
+
prhub status # "daemon: healthy" within a minute
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
To try it without installing a service, run `prhub daemon` in a terminal. Uninstall: on macOS,
|
|
74
|
+
`launchctl bootout gui/$(id -u)/dev.prhub.daemon` and delete `~/Library/LaunchAgents/dev.prhub.daemon.plist`; on
|
|
75
|
+
Linux, `systemctl --user disable --now prhub` and delete `~/.config/systemd/user/prhub.service`.
|
|
76
|
+
|
|
77
|
+
## Pull requests
|
|
78
|
+
|
|
79
|
+
`prhub wait <pr> --until <condition>[,<condition>…]` returns as soon as any one condition holds.
|
|
80
|
+
|
|
81
|
+
| condition | met when |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `done` | open, not a draft, CI SUCCESS, merge state CLEAN (configurable), and an APPROVED review **of the current head** that is newer than the head commit |
|
|
84
|
+
| `green` | the CI rollup is SUCCESS |
|
|
85
|
+
| `checks` | CI finished, whatever the result |
|
|
86
|
+
| `merged` | the PR was merged |
|
|
87
|
+
| `closed` | the PR was merged or closed |
|
|
88
|
+
| `review` | a new review arrived after the wait started |
|
|
89
|
+
| `change` | anything relevant changed: state, draft, CI, review, merge state, head commit |
|
|
90
|
+
|
|
91
|
+
`done` deliberately does not trust GitHub's `reviewDecision` alone: it stays APPROVED after a new push unless the
|
|
92
|
+
repository dismisses stale reviews, and a force-push with an unchanged patch relabels an old review onto the new
|
|
93
|
+
head. `prhub state` marks such a PR `review=APPROVED(stale: no approval of this head)`.
|
|
94
|
+
|
|
95
|
+
When CI goes red, `state` and `wait` also print the names of the failing checks.
|
|
96
|
+
|
|
97
|
+
## Commits: CI and deploys
|
|
98
|
+
|
|
99
|
+
The same daemon watches commits, in any repository, so agents stop polling `gh run watch` and the Actions API.
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
prhub wait acme/api@3a1e48e --until green # every CI run on the commit passed
|
|
103
|
+
prhub wait api@3a1e48e --until green --workflow deploy # only runs whose name contains "deploy"
|
|
104
|
+
prhub wait https://github.com/acme/api/commit/3a1e48e --until deployed --env production
|
|
105
|
+
prhub state @3a1e48e # a commit of the clone you are in
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| condition | met when |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `checks` | every run on the commit finished, whatever the result |
|
|
111
|
+
| `green` | every run passed (exits `3` as soon as one fails) |
|
|
112
|
+
| `deployed` | the commit is live in `--env` (default `production`, or `any`) |
|
|
113
|
+
| `change` | anything about the commit's runs or deploys changed |
|
|
114
|
+
|
|
115
|
+
It works the same whatever a repository's workflow names, default branch or release style:
|
|
116
|
+
|
|
117
|
+
- **CI** is every check suite that ran something on the commit. Suites from apps that never start a run (they sit
|
|
118
|
+
QUEUED forever) and cron-triggered runs (they attach to whatever the branch head is) are left out. A run
|
|
119
|
+
cancelled because a newer run of the same workflow replaced it is not a failure.
|
|
120
|
+
- **Deployed** reads [GitHub Deployments](https://docs.github.com/en/rest/deployments), plus commit statuses named
|
|
121
|
+
like `deployed/<env>`. A successful deploy of a *later* commit counts when that commit contains yours (asked once
|
|
122
|
+
per new deployment through the compare API), so nightly releases, batched deploys and a cancelled deploy followed
|
|
123
|
+
by a newer one all answer correctly. If a repository records no Deployments, use
|
|
124
|
+
`--until green --workflow <name of its deploy workflow>`.
|
|
125
|
+
|
|
126
|
+
A commit stays tracked for 6 hours after the last `state` or `wait` that asked about it.
|
|
127
|
+
|
|
128
|
+
## Naming things
|
|
129
|
+
|
|
130
|
+
| you type | means |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `https://github.com/acme/api/pull/42`, `acme/api#42` | a PR |
|
|
133
|
+
| `42` | PR 42 of the clone in the current directory |
|
|
134
|
+
| `api #42`, `api#42` | a PR, by repo name or nickname (see below) |
|
|
135
|
+
| `https://github.com/acme/api/commit/3a1e48e`, `acme/api@3a1e48e` | a commit (7 to 40 hex characters) |
|
|
136
|
+
| `api@3a1e48e`, `@3a1e48e` | a commit, by nickname, or in the current clone |
|
|
137
|
+
|
|
138
|
+
Nicknames come from `[aliases]` in the config, and from the names of the git clones found under `clone_roots`.
|
|
139
|
+
|
|
140
|
+
## Exit codes
|
|
141
|
+
|
|
142
|
+
`0` the condition was met · `1` timed out · `2` bad arguments, or no such commit · `3` the condition can never be
|
|
143
|
+
met (the PR closed; for a commit, a run failed under `green` or its deploy failed) · `4` the daemon is not running.
|
|
144
|
+
|
|
145
|
+
## Agent setup
|
|
146
|
+
|
|
147
|
+
**Tell your agents.** Add something like this to your agents' instructions (`CLAUDE.md`, `AGENTS.md`, …); a
|
|
148
|
+
longer version is in [docs/agents.md](docs/agents.md):
|
|
149
|
+
|
|
150
|
+
```markdown
|
|
151
|
+
## Waiting on GitHub: use prhub, never poll
|
|
152
|
+
Every agent on this machine shares one GitHub token. To wait for a PR, CI or a deploy, run
|
|
153
|
+
`prhub wait <pr or owner/repo@sha> --until <condition>` (in the background if you need to keep working), and use
|
|
154
|
+
`prhub state <pr>` or `prhub list --ready` for the current state. These read local files and cost nothing.
|
|
155
|
+
Never loop over `gh pr checks`, `gh pr view`, `gh run watch` or `gh api` with `sleep`.
|
|
156
|
+
If `prhub status` says PAUSED, that is a GitHub rate limit: keep waiting, don't poll by hand.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Enforce it (Claude Code).** `prhub hook claude` is a `PreToolUse` hook that refuses `gh pr checks --watch`,
|
|
160
|
+
`gh run watch`, `watch … gh …` and shell loops that `sleep` around `gh pr`, `gh run` or `gh api`, and tells the
|
|
161
|
+
agent which `prhub wait` to use instead. One-off reads and every write pass through untouched, and the hook stands
|
|
162
|
+
aside entirely when the daemon is not healthy. Add it to `~/.claude/settings.json`:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{ "hooks": { "PreToolUse": [ { "matcher": "Bash",
|
|
166
|
+
"hooks": [ { "type": "command", "command": "prhub hook claude", "timeout": 5 } ] } ] } }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Put `PRHUB_ALLOW=1` anywhere in a command to let one loop through on purpose.
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
`~/.config/prhub/config.toml` (override the path with `PRHUB_CONFIG`). Every key is optional:
|
|
174
|
+
|
|
175
|
+
```toml
|
|
176
|
+
search = "is:pr is:open author:@me org:acme" # what the daemon discovers by itself
|
|
177
|
+
interval = 60 # seconds between polls
|
|
178
|
+
discover_every = 180 # seconds between discovery searches
|
|
179
|
+
clone_roots = ["~/code"] # clones under here become repo nicknames
|
|
180
|
+
|
|
181
|
+
[aliases]
|
|
182
|
+
api = "acme/api" # nickname -> owner/repo, or the name of a local clone
|
|
183
|
+
web = "acme/web-app"
|
|
184
|
+
|
|
185
|
+
[done]
|
|
186
|
+
merge_states = ["CLEAN", "HAS_HOOKS"] # merge states that count as mergeable for `done`
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Any PR or commit an agent waits on is tracked automatically, even if the search would not find it (someone else's
|
|
190
|
+
PR, another organization). Closed PRs drop out an hour after they close.
|
|
191
|
+
|
|
192
|
+
## Files
|
|
193
|
+
|
|
194
|
+
Everything lives in `~/.local/state/prhub` (override with `PRHUB_HOME`):
|
|
195
|
+
|
|
196
|
+
| path | what |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `prs/<owner>__<repo>__<n>.json` | one record per PR: a summary plus the raw GraphQL fields |
|
|
199
|
+
| `commits/<owner>__<repo>__<sha>.json` | one record per watched commit |
|
|
200
|
+
| `watch.json`, `commit-watch.json` | what agents asked to track, and who asked (`PRHUB_CALLER`, else a tmux or Herdr pane id) |
|
|
201
|
+
| `events.jsonl` | every state change, one JSON line each |
|
|
202
|
+
| `daemon.json`, `daemon.log` | daemon health, the GitHub budget left, and the log |
|
|
203
|
+
|
|
204
|
+
Other tools can read these too. A dashboard, a merge queue or a status bar can use them without calling GitHub.
|
|
205
|
+
|
|
206
|
+
## Limits
|
|
207
|
+
|
|
208
|
+
- github.com only for now; GitHub Enterprise Server is not supported.
|
|
209
|
+
- One daemon per machine and per GitHub token. Agents on other machines need their own daemon.
|
|
210
|
+
- The state is up to one poll interval old (60 s by default). `prhub state --fresh` asks for a poll first.
|
|
211
|
+
- `done` checks the review decision, CI and merge state that GitHub reports. If your merge rules need more (a
|
|
212
|
+
specific reviewer, resolved threads), check those before merging.
|
|
213
|
+
|
|
214
|
+
## Contributing
|
|
215
|
+
|
|
216
|
+
Bug reports and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md). Report security issues
|
|
217
|
+
privately, as described in [SECURITY.md](SECURITY.md).
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
[MIT](LICENSE)
|