A terminal dashboard for the GitHub pull requests you have open, across every repository your account can see.
The list on the left shows each pull request with a coloured dot for its CI state, its age, repository, branches, number, review state and diff size. The pane on the right shows the selected pull request's WATCH summary and individual GitHub Actions checks.
prutil can turn a pull request into a lightweight feedback loop instead of a static page you have to keep checking. Watch a pull request and it polls for new review feedback and failed checks, deduplicates work it has already handed off, and routes actionable work to the matching coding-agent workspace through herdr. The agent triages the feedback, separates real issues from noise (including automated review noise), makes a focused fix, pushes a follow-up commit, and lets GitHub run the checks again.
The loop is deliberately human-in-the-loop: prutil observes and coordinates; the agent proposes changes through normal commits and pull-request feedback, while people retain control of review, merge, retries and ambiguous decisions.
sequenceDiagram
participant GitHub
participant prutil
participant Herdr
participant Agent as Coding agent
GitHub-->>prutil: New review feedback or failed checks
prutil->>GitHub: Poll and identify actionable changes
prutil->>Herdr: Route feedback to the matching repository and branch
Herdr->>Agent: Start or notify the appropriate workspace
Agent->>Agent: Triage feedback and decide what to fix
Agent->>GitHub: Push a focused follow-up commit
GitHub-->>prutil: Re-run checks and expose new feedback
prutil-->>Herdr: Continue the loop when more action is needed
-
Go 1.26 or newer, if you are building from source.
-
The gh CLI, installed and logged in:
gh auth login
prutil shells out to gh, so it uses your existing gh credentials and never
stores a token of its own. If you would rather use a personal access token,
export GH_TOKEN (or GITHUB_TOKEN) and gh will pick it up. GH_HOST selects
a GitHub Enterprise host.
task install # tidy, vet, lint, test, build and go installor, without go-task:
go install github.com/relloyd/prutil/cmd/prutil@latestprutil
prutil -limit 20
prutil -query 'is:open is:pr author:@me org:acme sort:created-desc'| Flag | Default | Meaning |
|---|---|---|
-query |
is:open is:pr author:@me archived:false sort:created-desc |
the GitHub search used to find pull requests |
-limit |
100 | how many pull requests to load |
-closed-query |
is:pr author:@me is:closed archived:false sort:updated-desc |
the search used by the recently closed view |
-closed-per-repo |
3 | how many closed pull requests any one repository contributes |
-closed-repo-limit |
30 | how many repositories the closed view may query individually |
-skip-auth-check |
false | skip the gh auth status check at startup |
-dry-run |
false | record what would be sent to a coding agent without sending it |
-mouse |
true | click to select and scroll with the wheel; -mouse=false leaves the terminal its own wheel and drag-to-select |
-version |
print the version and exit |
| Key | Action |
|---|---|
| left click | select a pull request in the list |
j / k or ↓ / ↑ |
move within the focused pane; in detail, select WATCH or CHECKS |
g / G or home / end |
jump to the first or last item |
l or → |
focus detail from the list, or drill into the selected detail section |
h, ← or esc |
go back one level |
enter |
open the selected pull request, or the selected check, in your browser |
y or c |
copy the selected pull request's URL, or the selected check's, to the clipboard |
r |
refresh from GitHub |
a |
auto-refresh: reload every 30s, five times over. press again to add five more |
w |
watch the selected pull request, or stop watching it |
W |
hand the selected pull request's open review feedback to a coding agent now, creating one when needed |
F |
investigate the selected pull request's failed checks now; this never creates an agent |
R |
trigger an AI review on the selected open pull request by posting the configured comment |
N |
check the selected open pull request for new review feedback and notify an existing agent |
tab |
switch between your open and your recently closed pull requests |
? |
open the shortcut overlay: type to filter, enter to run the highlighted shortcut, esc or ? to close |
q or ctrl+c |
quit |
Below 80 columns the two panes collapse into one: the list fills the terminal,
clicking a row selects it, l swaps to detail, and h swaps back. When WATCH
is available, select it above CHECKS and press l again to see its full
schedule, activity and handoff details.
The footer has one line, so it lists the actions and leaves moving about to the
arrow keys. ? opens an overlay listing every shortcut with a sentence on what
it does. Start typing to fuzzy-filter it (agent, copy, or a key such as W),
move with ↑ and ↓ or half a page at a time with ctrl+d and ctrl+u, press
enter to run the highlighted shortcut, and esc to return. While it is open,
keys go to the filter, so q types rather than quits; ctrl+c still quits.
Copying uses whichever clipboard program your platform provides: pbcopy on
macOS, clip on Windows, and wl-copy, xclip or xsel on Linux, whichever
is installed first. If none is, prutil says which ones it looked for.
a reloads the view on screen every 30 seconds, five times, and then stops.
That is two and a half minutes of watching a pull request's checks turn green
without touching the keyboard. Press a again at any point during the run and
another five reloads are added, so a long CI run is a matter of topping the
counter up rather than holding a mode open. The header counts down what is
left, and once it runs out prutil is back to refreshing only when you press
r.
Each automatic reload is the same work r does, so it costs the same one
request for the list plus the checks it warms.
w marks a pull request as watched. The row grows a ◉, the header counts how
many are marked, and the mark survives quitting: it is kept in prutil's own
directory, not in the terminal.
From then on prutil watches that pull request for review feedback, and when
some appears it gives it to a coding agent through
herdr. Open feedback means a review thread that is neither
resolved nor already answered by you, so a conversation you have had the last
word in is left alone. W does the same thing on demand, for a pull request
you have not armed or one you want looked at again now.
Watching also monitors the pull-request check rollup. When the rollup fails,
prutil fetches the individual checks and waits until every check is terminal.
It then sends all failed checks together to an existing matching Herdr agent so
the agent can decide whether they share a cause. The default check prompt asks
the agent to fix failures related to the pull request with a follow-up commit,
re-trigger unrelated failures with gh, and ask for human assistance when it
has already retried an unchanged check or is unsure what to do. A missing agent
is recorded and notified, but does not cause automatic workspace provisioning.
F forces the same investigation immediately using the failures currently
known, even while other checks are pending. It bypasses automatic
deduplication but still requires an existing agent. When a failed check is
selected, W sends the same investigation while retaining W's explicit
permission to provision a workspace and start an agent if needed.
Automatic investigations are recorded against the head commit, so a restart does not resend the same failure. Pushing a new head allows a new investigation. Stopping and starting the watch clears that remembered head and intentionally allows the current failures to be investigated again.
To test watcher delivery with a code-line comment of your own, put this exact marker in either the opening comment or your latest reply's Markdown source:
<!-- prutil:test -->GitHub hides the marker when it renders the comment. While that thread remains unresolved, prutil treats it as feedback even though you wrote the latest comment. The marker only works when it is in a comment written by the authenticated viewer: either the thread's opening comment or the viewer's latest reply. It does not opt in an ordinary pull-request conversation comment, a marker written by another reviewer, or an older reply that is no longer the latest. Normal duplicate suppression still applies, so the thread is handed over again only when it gains a new latest comment.
N is a diagnostic trigger for the automatic path. It asks GitHub for the
selected open pull request's review threads and sends only feedback prutil has
not handed over before. It never provisions a checkout, worktree, workspace,
or agent, so it is useful for confirming the normal no-agent and herdr
notification behavior without waiting for the watcher to spot a change.
prutil picks the agent rather than asking you to. It lists the agents herdr knows about, reads the repository and branch out of each one's working directory, and prefers the one sitting on the pull request's head branch. An agent one branch away is used too, and told so in the prompt. The terminal prutil is itself running in is never given work. If the agent is busy prutil waits for it to finish, up to fifteen minutes, and if it is stuck at a prompt of its own nothing is sent at all.
W is also the explicit consent to set up a workspace when no suitable agent
exists. It resolves a local checkout, fetches the pull request's
pull/<number>/head ref, reopens an existing matching herdr worktree when it
can, or creates a no-focus worktree workspace and starts the configured agent
there. Set herdr.agent_kind to the herdr agent kind to start (for example
claude or copilot); it is required only for this creation path. The
automatic watcher never creates a Git worktree, herdr workspace, or agent: it
continues to report that no local agent was available.
Nothing is ever handed over twice. Each thread prutil sends is remembered
against the comment it ended on, so pressing W again on a review whose
comments you have decided not to act on costs one GitHub request and sends
nobody anything. That is what makes it safe to leave a pull request watched.
Every attempt is recorded, sent or not, one JSON object per line, in
handoffs.jsonl beside the configuration. Start with -dry-run, which writes
that log and shows the status line without mutating a repository, worktree,
workspace, or agent.
The selected pull request's detail pane includes a WATCH section when it has
watch or handoff activity. It shows the current state-machine tier, cadence and
next check; work currently in flight; a bounded activity feed from this TUI
session; and the three most recent durable handoff attempts for that pull
request. The activity feed resets when prutil exits; handoffs.jsonl is the
cross-session record.
Watching a pull request is two questions, asked at very different rates.
The first is cheap and batched. One GraphQL request covers every watched pull request at once, whatever repositories they are spread across, and reads only enough to notice that something moved: the head commit, the check state, the last-updated time, the conversation-comment total, and the total review-thread count. It is one rate limit point per request, and one request per hundred pull requests you have marked, which is where GitHub caps the node lookup it uses.
The second is the expensive one, and it is asked only of the pull requests the first one flagged, or of one that has gone five polls without being asked. That second part matters: a reply inside an existing review thread moves neither count, so a counter on its own would miss it.
That precise read covers the first 100 code-review threads. For each one it reads the opening comment's author and body plus the newest comment's author, id and body. The newest body is fetched only so the self-test marker can be used in the viewer's latest reply; arbitrary historical replies are not scanned. The top-level pull-request conversation-comment count is only a change signal; it is not a code-review thread and is never handed to an agent.
How often the first question is asked depends on what the pull request is doing:
| The pull request | Asked about |
|---|---|
| has checks running | every 30 seconds |
| has nothing in progress | after 2 minutes, then 4, 8, 16, and 30 |
| has been given to an agent | after 10 minutes, then 20, 40, and 60 |
| has not changed for about two hours | not at all |
Anything at all changing puts a pull request back to the top of that ladder. A
pull request prutil has stopped asking about is still armed, and its ◉ turns
hollow to say so; r wakes it, along with everything else, while R (triggering
an AI review) wakes that specific pull request.
prutil reads config.yaml from $PRUTIL_HOME, else $XDG_CONFIG_HOME/prutil,
else ~/.config/prutil. On first startup it creates a complete editable
template there, with these defaults. Every key remains optional:
herdr:
agent_kind: claude # required only when W starts a new agent
skill: pr-comment-triage # the skill the default prompt invokes
wait_for_idle: 15m # how long to wait for a busy agent
dry_run: false
toast: true # show a herdr notification alongside each handoff
# Optional separate Go template for failed-check investigations. It receives
# Repo, Number, URL, Title, HeadRef, BaseRef, Checks and Note.
check_prompt: "..."
watch:
active_interval: 30s # while checks are still running
base_interval: 2m # once nothing is in progress
max_interval: 30m # where the backoff stops growing
notified_interval: 10m # after a handoff, when an agent is at work
max_notified_interval: 60m
idle_interval: 10s # how often a busy agent is re-read
dormant_after: 3 # polls at the cap before prutil stops asking
force_precise_every: 5 # polls before the expensive question is asked anyway
self_test_marker: "<!-- prutil:test -->" # "" turns it off
review:
comment: "/gemini review" # comment posted by R to trigger an AI review; "" turns it off
repos:
acme/widgets: "@coderabbitai review" # optional per-repository override
repos:
acme/widgets: ~/src/widgets # optional explicit checkout for W
discovery:
roots:
- ~/src # optional roots scanned after repos missesDiscovery roots are walked four levels deep, and a checkout's origin remote is
read from its own .git/config before git is asked about it, so pointing at a
directory of a hundred repositories costs a hundred small file reads rather
than several hundred forked processes.
With skill set, the prompt is /<skill> <pull request url>. Without it,
prutil spells the job out instead. Either can be replaced with herdr.prompt,
a Go template given Repo, Number, URL, Title, HeadRef, BaseRef,
Skill, UnresolvedCount, NewCount and Note. Failed-check handoffs use
herdr.check_prompt, whose Checks value contains the failed check entries
and whose default prompt is designed for deciding between a follow-up commit,
a retry, and human assistance.
Explicit repos entries win. When none exists, prutil checks its private
repos.json cache and then scans discovery.roots, validating every candidate
against its origin remote before it can be used. Stale cache paths are ignored
and refreshed. GitHub poll intervals are clamped to fifteen seconds at the
shortest, so a typo cannot turn a dashboard into a load test.
tab switches the list between your open pull requests and your recently
closed ones. Each view keeps its own cursor and scroll position, and the closed
view is not fetched until the first time you show it.
The closed view is grouped: no repository contributes more than
-closed-per-repo rows, so a single busy repository cannot fill the screen and
hide everywhere else you have been working.
If GitHub refuses some of those per-repository queries, which a very large
organisation can provoke, the header says how many repositories were
unreachable and the rest of the list is shown anyway. Press r to try again.
Note that -closed-query inherits archived:false from the open view. If most
of your history is in repositories that have since been archived, drop that
qualifier to see it:
prutil -closed-query='is:pr author:@me is:closed sort:updated-desc'The headline list is one GraphQL request, which includes the check rollup that
colours each dot, so the first screen appears after a single round trip. The
individual checks are fetched afterwards, in the background for the top of the
list and on demand as you move, with at most four gh processes at a time. Both
are cached until you press r.
The closed view usually costs one request too. It sweeps your closed pull requests newest first, and when that sweep reaches the end of the search it already holds everything, so the grouping is exact and nothing more is fetched. Only when the sweep runs out of room first does it go further. It reads a little further down the same search, collecting nothing but repository names, then queries the ones that came up short directly. Nothing enumerates an organisation's repositories, so the work does not grow with the size of the organisations you belong to. Those per-repo searches are batched under GraphQL aliases, so three repositories cost one request and one rate limit point.
Batches are small on purpose. GitHub gives a single GraphQL document about ten seconds before returning a 502, and that budget is spent faster in a large organisation, so the closed view trades round trips for headroom.
0 comments
log in to comment.