skills/gh/SKILL.md
GitHub CLI — source control, CI, code review, and issues for OSS and personal repos
npx skillsauth add athal7/dotfiles ghInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
GitHub CLI (gh) gotchas that --help won't tell you.
gh pr view --jsonFor plain comment reads, use gh pr view N --json comments,reviews --jq ... (or gh issue view) instead of gh api repos/.../pulls/N/comments. The gh pr view form is allow-listed as read-only; gh api is on ask because it accepts -X POST/PATCH/DELETE.
# Top-level PR comments
gh pr view "$PR" --json comments --jq '.comments[] | {user: .author.login, body: .body}'
# Inline review comments
gh pr view "$PR" --json reviews --jq '.reviews[] | {user: .author.login, state, body: .body}'
Field names differ from REST: .author.login (not .user.login), no commit_id or in_reply_to_id. Drop down to gh api .../comments only when you need those REST-only fields (e.g. the automated-review flow below).
Bucket cross-repo PR work on latestReviews[].state and mergeStateStatus:
gh api graphql -f query='{
viewer { pullRequests(first: 50, states: OPEN) { nodes {
number title url isDraft mergeStateStatus
repository { nameWithOwner }
reviewRequests(first: 20) { nodes { requestedReviewer { ... on User { login } ... on Bot { login } } } }
latestReviews(first: 20) { nodes { state author { login } submittedAt } }
commits(last: 1) { nodes { commit { committedDate } } }
} } }
search(query: "is:pr is:open review-requested:@me", type: ISSUE, first: 50) { nodes { ... on PullRequest {
number title url repository { nameWithOwner }
} } }
}'
Bucket your authored PRs (closest-to-done first). lastCommit = commits[-1].commit.committedDate:
| Bucket | Rule |
|---|---|
| conflict | mergeStateStatus == DIRTY — fix first |
| ci-failing | mergeStateStatus == UNSTABLE |
| review-to-address | any latestReviews[].state in (CHANGES_REQUESTED, COMMENTED) with submittedAt > lastCommit — bots count |
| ready-to-merge | mergeStateStatus == CLEAN AND any APPROVED in latestReviews[].state AND no CHANGES_REQUESTED/COMMENTED newer than lastCommit |
| waiting-for-review | none of the above; reviewRequests non-empty or no review yet |
The search block returns others' PRs requesting your review (review-requested). For mentions and assigned issues without PRs, use gh status (--org <org> to scope, -e <owner/repo> to exclude).
mergeStateStatus values| Value | Means |
|---|---|
| CLEAN | No conflicts, branch protection satisfied |
| DIRTY | Merge conflicts |
| UNSTABLE | CI failing |
| BLOCKED | Branch protection unsatisfied (e.g. required approval missing) — not a conflict |
| BEHIND | Behind base branch |
| HAS_HOOKS | Mergeable with passing status and pre-receive hooks |
| UNKNOWN | Retry |
gh pr list --reviewer @me returns empty even when review requests exist — use the GraphQL search block above (or gh search prs --review-requested=@me).gh search prs --reviewed-by @me returns empty — for PRs you've reviewed, query per-repo with gh pr list --json author,reviews and filter reviews[].author.login == @me AND author.login != @me.gh is the transport for GitHub-hosted automated reviewers. Per-org config lives in ~/.local/share/chezmoi/.chezmoidata/local.yaml under orgs.<org>.automated_review (see local.yaml.example in the dotfiles repo root for schema). Orgs without that block have no automated review available.
Run this exact command. Do not infer availability from collaborator lists, repo settings, prior PRs, or anything else.
ORG=$(gh repo view --json owner -q '.owner.login')
chezmoi data --format json | jq -r ".orgs[\"$ORG\"].automated_review // empty"
bot_login, auto_runs, and trigger. Proceed with the trigger/wait/fetch flow below.Common wrong checks that will give you a false negative:
gh api — the bot may be installed at GitHub-account level rather than per-orgPull all reviews authored by the configured bot_login. Most recent first; take [0] for the latest:
gh api "repos/$OWNER/$REPO/pulls/$PR/reviews" --paginate \
--jq "[.[] | select(.user.login == \"$BOT_LOGIN\")] | sort_by(.submitted_at) | reverse"
Each review has commit_id (the head SHA at review time — needed for staleness checks) and submitted_at. Inline comments live separately:
gh api "repos/$OWNER/$REPO/pulls/$PR/comments" --paginate \
--jq "[.[] | select(.user.login == \"$BOT_LOGIN\") | {path, line, body, id, in_reply_to_id, commit_id}]"
Reply threads on a comment use in_reply_to_id chains.
Use only the commands below. Do not invent alternatives. The REST endpoint POST /repos/{o}/{r}/pulls/{n}/requested_reviewers returns 422 "Reviews may only be requested from collaborators" when given copilot-pull-request-reviewer — the bot is not a collaborator. Only gh pr edit --add-reviewer "@copilot" works, because @copilot is a special handle that gh resolves client-side (see gh pr edit --help).
The trigger field returned by the availability check tells you which form. Parse it and run the corresponding command — do not hardcode the handle or text:
add_reviewer @handle → gh pr edit $PR --add-reviewer "@handle"comment @handle <text> → gh pr comment $PR --body "@handle <text>"TRIGGER=$(chezmoi data --format json | jq -r ".orgs[\"$ORG\"].automated_review.trigger")
FORM="${TRIGGER%% *}" # "add_reviewer" or "comment"
ARGS="${TRIGGER#* }" # everything after the first word
if [[ "$FORM" == "add_reviewer" ]]; then
gh pr edit "$PR" --add-reviewer "$ARGS"
else
gh pr comment "$PR" --body "$ARGS"
fi
Triggering is a public action — it appears in the timeline. Announce it before doing it.
After triggering, verify the bot was actually added as a reviewer before waiting:
gh api "repos/$OWNER/$REPO/pulls/$PR/requested_reviewers" --jq '.users[].login, (.users[] | select(.type=="Bot") | .login)'
Note: Copilot appears in requested_reviewers as login Copilot (a Bot user, app slug copilot-pull-request-reviewer), but its review submissions use login copilot-pull-request-reviewer[bot] — the bot_login from the availability check matches the review author, not the requested-reviewer login.
After triggering (or to wait for an auto-running bot), poll until a review exists with a commit_id matching the current HEAD (or just newer than since_sha). Reasonable cadence: every 5s, timeout 120s.
TARGET_SHA=$(gh pr view "$PR" --json headRefOid -q .headRefOid)
for i in {1..24}; do
LATEST=$(gh api "repos/$OWNER/$REPO/pulls/$PR/reviews" --paginate \
--jq "[.[] | select(.user.login == \"$BOT_LOGIN\")] | sort_by(.submitted_at) | reverse | .[0].commit_id")
[[ "$LATEST" == "$TARGET_SHA" ]] && break
sleep 5
done
Don't re-trigger if the human author dismissed the bot without a fix and the diff hasn't materially changed. Check review timeline events:
gh api "repos/$OWNER/$REPO/pulls/$PR/reviews" --paginate \
--jq "[.[] | select(.user.login == \"$BOT_LOGIN\") | {state, dismissed: .state == \"DISMISSED\", submitted_at, commit_id}]"
state == "DISMISSED" with no follow-up review means the author chose not to act on it.
gh repo view --json visibility -q '.visibility'
# Latest run on a branch
gh run list --branch <branch> --limit 1
# Failed step logs
gh run view <run-id> --log-failed
gh pr review doesn't support inline comments on specific lines. Use gh api instead.
Use --input - with heredoc JSON (not -f 'comments=[...]' which gets stringified):
gh api repos/{owner}/{repo}/pulls/{pr_number}/reviews \
--method POST \
--input - << 'EOF'
{
"event": "REQUEST_CHANGES",
"comments": [
{"path":"file.rb","line":10,"body":"Comment text"},
{"path":"other.rb","line":25,"body":"Another comment"}
]
}
EOF
Event types: COMMENT, APPROVE, REQUEST_CHANGES.
When approving with non-blocking feedback (nits), disable auto-merge so the author has a chance to address them before the merge lands:
gh pr merge --disable-auto {pr_number}
Do this before posting the review. If auto-merge wasn't enabled, the command is a no-op that exits cleanly.
{"path":"file.rb","start_line":5,"line":10,"body":"This block..."}
When addressing reviewer comments after making fixes:
Push before resolving. GitHub ties thread resolution to the commit that addressed it.
gh api graphql -f query='
query($owner:String!,$repo:String!,$pr:Int!) {
repository(owner:$owner,name:$repo) {
pullRequest(number:$pr) {
reviewThreads(first:100) {
nodes {
id isResolved path line
comments(first:5) {
nodes { id body author { login } }
}
}
}
}
}
}' -f owner=OWNER -f repo=REPO -F pr=NUMBER
gh api graphql -f query='
mutation($id:ID!) {
resolveReviewThread(input:{threadId:$id}) {
thread { isResolved }
}
}' -f id=PRRT_kwDOxxxxxxx
Use the REST API with the top comment's numeric ID:
gh api repos/{owner}/{repo}/pulls/{pr_number}/comments/{comment_id}/replies \
--method POST \
-f body="Intentionally left as-is because..."
gh api repos/{owner}/{repo}/pulls/comments/{id} --method DELETEbody field to skip summary commentpath, line, bodygh issue handles GitHub Issues for OSS and personal repos (non-Linear trackers). Use gh issue create, gh issue list, gh issue view, and gh issue edit. GitHub issue templates live in .github/ISSUE_TEMPLATE/*.md — gh issue create --template only works interactively; read the template file directly and pass content via --body-file.
development
Zoom meeting captions — file locations and format
tools
macOS dictation custom vocabulary — sync knowledge base names and terms to the system spelling dictionary
testing
Look up people, projects, products, and decisions locally first: contact info (email, Slack ID, GitHub handle), titles and teams, project/product status, who works on what, and past decisions. Check before searching Slack, email, calendar, or GitHub — this is the first stop for any contact detail, project context, or decision-history question.
testing
Communication style, audience awareness, and AI-authorship markers for human-facing prose — load when composing chat messages, review comments, merge request descriptions, emails, doc bodies, or ticket descriptions