twgh
Read-only GitHub reviewer CLI — for humans and agents.
Triage what needs you, re-review only what changed since you last looked (force-push robust), and see which of your comments were addressed. Works across github.com and GitHub Enterprise.
twgh never writes to GitHub — no approvals, no comments, no merges. It is the read side of reviewing; for write actions it hands you off to the browser.
Every command produces clean, pipeable output: --json where the data is
tabular, unified diff hunks where the data is code. No wrapper needed —
twgh status --json | jq and twgh diff 42 | llm "summarise" work
out of the box. This makes twgh equally useful as a human terminal tool
and as a building block in agent and CI pipelines.
GitHub concepts
Understanding these GitHub primitives helps clarify what twgh surfaces and why.
Comment types on a PR
┌─────────────────────────────────────────────────────────────┐
│ GitHub PR Comments │
├──────────────────────────┬──────────────────────────────────┤
│ Issue Comments │ Review Threads │
│ (PR-level / timeline) │ (inline / code-level) │
├──────────────────────────┼──────────────────────────────────┤
│ • Not tied to a file │ • Anchored to a file + line │
│ • No commit reference │ • Tied to a specific commit OID │
│ • Cannot be "resolved" │ • Can be resolved/unresolved │
│ • Cannot be "outdated" │ • Can become outdated when the │
│ • General conversation │ underlying code changes │
│ on the PR timeline │ • Groups into a thread (replies)│
└──────────────────────────┴──────────────────────────────────┘
| Level | What it is | GitHub API source | In twgh |
|---|---|---|---|
| Issue comment | Free-form message on the PR timeline ("LGTM!", "please rebase") | pr.comments.nodes |
comments; complete timeline in review-context |
| Review thread | A conversation anchored to a specific line of code, started during a review | pr.reviewThreads.nodes |
threads command (one row per thread) |
| Review comment | An individual reply within a review thread | comments inside a thread node | comments command (one row per comment) |
A review itself (the APPROVE / REQUEST_CHANGES / COMMENT submission) is a
separate entity. twgh uses its commit for anchor calculation and exposes
non-empty review bodies in review-context.
Reviews and review state
When you submit a review on GitHub, it records:
- State:
APPROVED,CHANGES_REQUESTED, orCOMMENTED - Commit OID: the HEAD commit at the time you submitted
twgh uses this commit OID as the anchor — the baseline for twgh diff.
Everything between anchor and current HEAD is "what changed since you last
looked". Because the anchor is a commit hash (not a branch pointer), it
survives force-pushes and rebases.
Resolved vs outdated threads
- Resolved: someone explicitly clicked "Resolve conversation". The feedback loop is considered closed.
- Outdated: the code the comment points at has moved (the diff hunk no longer exists at that position). GitHub marks the thread but does not auto-resolve it.
twgh's classify_thread maps these states plus the commit history into a
verdict: ✓ resolved, ~ changed (code moved but not resolved), or
· untouched (nothing happened).
Force-push safety
Many review tools break when authors force-push because branch history is
rewritten. twgh avoids this by anchoring on commit OIDs returned by the
GraphQL API — these point at the original commits your review was filed
against. GitHub's compare endpoint (/compare/<oid>..<oid>) works with any
reachable object, including pre-rebase commits that are no longer on any branch.
PR references
<ref> is a PR URL, host/owner/repo#N, owner/repo#N, owner/repo/N, or a
bare number inside a repo checkout. status prints URLs — paste them straight
back.
Host selection
-H/--gh-host overrides $GH_HOST, which defaults to github.com. Works as a
global option (twgh -H ghe.example.com status) or per command.
Installation, Configuration
Optional TOML config at ~/.config/twgh/config.toml (override with --config).
Bootstrap a starter with:
# Installlation
uv tool install twgh # or: pipx install twgh
make install # uv tool install -e . + bash completion
# Configuration
twgh edit --init
Hide PRs from bots/service accounts in status and threads (inbox mode):
[exclude]
authors = ["dependabot[bot]", "self-hosted-renovate[bot]"]
# Per-host additions (extends the global list):
[exclude."github.enterprise.com"]
authors = ["renovate-enterprise[bot]"]
Override the exclusion list to show a specific author:
twgh status --author "dependabot[bot]"
Prerequisites
- The GitHub CLI
gh, authenticated for each host you review on:gh auth login --hostname <host>. twgh borrows gh for auth, host routing, and transport; it never handles your token.
Commands
twgh status — which PRs need me right now?
twgh status
twgh status --json
twgh status --author "dependabot[bot]"
Cross-repo rollup of every open PR you are review-requested on, commented on,
or have reviewed. Per PR: author, your review state against the current head
(needs_review / needs_re_review / up_to_date), and open-thread count.
★ marks PRs that moved since your last status run — new head or new comments,
tracked via a local snapshot (the first run deliberately flags nothing).
| Option | Description |
|---|---|
--json |
Machine-readable JSON array — one object per PR |
--author <user> |
Show only PRs by this author (overrides exclusion list) |
twgh diff — what changed since I last looked?
twgh diff <ref>
twgh diff <ref> --all
twgh diff <ref> --comments
twgh diff <ref> --web
The author pushed again; you don't want to re-read the whole PR. diff anchors
on your last submitted review (else your last inline comment) and shows
anchor→HEAD, restricted to the files you commented on. The anchor is a commit
OID, so the diff survives force-pushes and rebases. With no prior review there
is nothing to re-diff — it points you at twgh open instead.
Output is unified diff text, paged through delta / $PAGER / less -R.
When stdout is not a TTY (piped or redirected), the pager is skipped and raw
diff text goes to stdout — ready for grep, patch, or an LLM.
| Option | Description |
|---|---|
--all |
Show the whole-PR diff, not just files you commented on |
--comments / -C |
Weave your inline review comments into the diff as # -prefixed context above each file's patch |
--web |
Open the anchor→HEAD range in GitHub's compare view instead of the pager. A force-push-safe deep-link the web UI can't construct by hand. File-scoping and --all do not apply |
twgh compare — diff between two arbitrary commits
twgh compare <base> <head>
twgh compare <base> <head> --web
twgh compare abc123 def456 --repo LOS/my-repo
Show the diff between any two commits. Both short and long SHA forms are
accepted — GitHub's API and web UI resolve them server-side. Repo context
comes from the current directory's git remote; use --repo to override.
Same output behaviour as diff: paged for humans, raw text when piped.
| Option | Description |
|---|---|
--web |
Open the compare view in the GitHub web UI (/compare/<base>..<head>) |
--repo / -R <owner/repo> |
Repository override — use instead of CWD git context |
twgh threads — were my comments addressed?
twgh threads <ref>
twgh threads # global inbox (no ref)
twgh threads <ref> --mine
twgh threads <ref> --by alice
twgh threads <ref> --all
An action-oriented triage view: one row per review conversation with a verdict showing whether it still needs attention:
✓ resolved— the thread is marked resolved~ changed— the anchored code moved since the comment (inspect withtwgh diff)· untouched— the point still stands as written
Omit <ref> for a global inbox of open threads across all PRs that involve
you.
| Option | Description |
|---|---|
--mine |
Only threads you opened |
--by <user> |
Filter by thread author |
--all |
Include resolved threads |
twgh comments — what was said while I was away?
twgh comments <ref>
twgh comments <ref> --mine
twgh comments <ref> --by carol
twgh comments <ref> --all
A chronological reading view of everything said on a PR: top-level comments and inline review comments, sorted by time. Think of it as a chat log — you read top to bottom to understand the full conversation timeline. Comments in resolved threads are hidden by default.
| Option | Description |
|---|---|
--mine |
Only your comments |
--by <user> |
Filter by comment author |
--all |
Include inline comments in resolved threads |
threads vs comments
Both draw from the same PR data but serve different workflows:
threads |
comments |
|
|---|---|---|
| Unit | one row per conversation | one row per comment |
| Order | by file path | by time |
| Key info | actionable verdict | full message body |
| Question answered | "what needs my response?" | "what was said?" |
| Scope | single PR or global inbox | single PR only |
twgh review-context — complete re-review evidence
twgh review-context owner/repo#42
twgh review-context https://ghe.example.com/group/repo/pull/42
twgh review-context # current branch's PR
twgh review-context 42 --reviewer alice
Emit one JSON document for agents and other re-review automation. The payload contains PR head/base metadata, every inline thread started by the reviewer with all replies, the reviewer's non-empty review bodies and commit anchors, and the complete PR-level comment timeline. All connections are paginated, including replies beyond the first 100 comments in a thread.
The reviewer defaults to the authenticated user on the PR's actual host.
--reviewer accepts login or @login. The payload carries
schema_version: 1; issue comments include by_reviewer because timeline
comments have no thread, resolution state, or commit anchor.
{
"schema_version": 1,
"pr": {
"host": "github.com",
"repo": "owner/repo",
"number": 42,
"url": "https://github.com/owner/repo/pull/42",
"title": "Fix retries",
"author": "alice",
"head_ref": "fix/retries",
"head_oid": "abc123",
"base_ref": "main"
},
"reviewer": "tom",
"summary": {
"inline_threads": 1,
"unresolved_threads": 0,
"review_bodies": 1,
"reviewer_issue_comments": 1
},
"inline_threads": [
{
"path": "src/retry.py",
"line": 42,
"is_resolved": true,
"is_outdated": true,
"reviewer_comment": {
"url": "https://github.com/owner/repo/pull/42#discussion_r1",
"body": "Handle timeouts",
"created_at": "2026-07-01T10:00:00Z",
"diff_hunk": "@@ -40,3 +40,4 @@",
"original_commit": "def456",
"commit": "def456"
},
"replies": []
}
],
"review_bodies": [
{
"body": "Please add a retry test",
"state": "CHANGES_REQUESTED",
"submitted_at": "2026-07-01T10:05:00Z",
"commit_oid": "def456"
}
],
"issue_comments": [
{
"author": "tom",
"by_reviewer": true,
"url": "https://github.com/owner/repo/pull/42#issuecomment-2",
"body": "Please document the fallback",
"created_at": "2026-07-01T10:10:00Z"
}
]
}
twgh open — hand off to the browser
twgh open <ref>
Opens the PR in the browser for the write actions twgh stays out of: approving, replying, resolving, merging.
twgh edit — configure twgh
twgh edit --init # create starter config and open in $EDITOR
twgh edit # open existing config in $EDITOR
Opens the config file in $EDITOR. Use --init to bootstrap a starter config
if one doesn't exist yet.
twgh summary — reviewer activity report
twgh summary # your reviews, last 30 days
twgh summary --days 7 # last week only
twgh summary --reviewer alice # someone else's activity
twgh summary --json # machine-readable output
Produces a three-tier report measuring a reviewer's effort, engagement depth, and responsiveness over a time window.
Volume metrics
| Metric | What it measures |
|---|---|
| PRs reviewed | Unique PRs where the reviewer submitted at least one review |
| Lines reviewed | Sum of additions + deletions across all reviewed PRs (proxy for review scope) |
| Files reviewed | Sum of changed files across all reviewed PRs |
| Total comments | Inline review comments authored by the reviewer across all threads |
| Comments / PR | Average comments per PR — measures thoroughness |
| Unique repos | How many distinct repositories the reviewer covers — breadth of impact |
Engagement metrics
Each time a reviewer submits a review on GitHub, it carries one of three states:
| State | Meaning |
|---|---|
| Approved | Reviewer signed off — could range from rubber-stamp to thorough |
| Changes requested | Reviewer took a position and authored specific feedback |
| Comment only | Reviewer engaged but did not commit to a verdict |
A single PR can accumulate multiple reviews (e.g., request changes → author pushes fixes → re-review and approve = 2 reviews on 1 PR). This is why "total reviews" can exceed "PRs reviewed".
Quality signals
Engagement score — a weighted average that rewards deeper review effort:
score = (changes_requested × 3 + comment_only × 2 + approvals × 1)
─────────────────────────────────────────────────────────────────
total_reviews
| Score | Interpretation |
|---|---|
| 1.0 | All approvals — potential rubber-stamping |
| 1.5 | Mix of approvals and substantive comments |
| 2.0 | Balanced engagement with meaningful discussion |
| 2.5–3.0 | Heavy change-request activity — very hands-on reviewer |
The weights reflect effort: requesting changes requires the reviewer to articulate specific problems and defend a position, while approving can be as simple as clicking a button.
Median turnaround — median time from PR creation to the reviewer's first review submission on that PR. Measures responsiveness — how long authors wait for initial feedback. This is a rough proxy: it penalises the reviewer for PRs created before they were assigned, or PRs they picked up late from a queue.
Output conventions
twgh uses two output formats, chosen by what the data naturally is:
| Data shape | Format | Commands |
|---|---|---|
| Tabular (PR list, thread verdicts, metrics) | Rich table (human) or --json (machine) |
status, threads, comments, summary |
| Re-review evidence | Versioned JSON object | review-context |
| Code delta | Unified diff text with @@ hunks |
diff, compare |
Diff output is already structured — @@ markers delimit hunks, +/- prefix
changed lines, file headers name each path. Any downstream tool, LLM, or agent
can parse it without a JSON wrapper. Piping works naturally: the pager is
skipped when stdout is not a TTY.
Development
make test # pytest + coverage (floor 85%)
make static-analysis # ruff lint-fix, format, ty
Metadata
Release files for twgh 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| twgh-0.7.0.tar.gz | 38.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| twgh-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 73.6 kB
Release files / twgh-0.7.0.tar.gz
| Download URL | twgh-0.7.0.tar.gz |
|---|---|
| Size | 38.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b8b098cbae44911a169fc133ee0d955eb0f83a09fa5a478e663c3e6737f2260e
|
|
BLAKE2b-256 checksum How to use checksums |
eb4e2edf4de6371d4c725456e51e9094661e20c23e722543cabaa8b292eecd89
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.11
|
Release files / twgh-0.7.0-py3-none-any.whl
| Download URL | twgh-0.7.0-py3-none-any.whl |
|---|---|
| Size | 35.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
54fac2ded3afd552e1010b053ca82e4306cad7bacb18235ffb1c9c65d4ad8306
|
|
BLAKE2b-256 checksum How to use checksums |
3108dabcba70800b4ba5539e99e90c339f5e12245c19abc554fe2f7ab5648697
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.11
|
Security for twgh 0.7.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.