GitHub Skill
Use these scripts instead of raw gh commands for consistent error handling and structured output.
Triggers
| Phrase |
Operation |
create a PR |
new_pr.py |
respond to review comments |
post_pr_comment_reply.py |
check CI status |
get_pr_checks.py / get_pr_check_logs.py |
close issue |
close_issue.py |
add label to issue |
set_issue_labels.py |
Transport Preflight
gh can be installed, hold a token, and still be refused for a whole session
(agent sandboxes that proxy egress do this). Decide the transport once, before
the first GitHub call:
# CLAUDE_PLUGIN_ROOT is set in a vendored install; falls back to .claude in-repo.
SCRIPTS_DIR="${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/github/scripts/utils"
python3 "$SCRIPTS_DIR/check_github_transport.py"
Non-POSIX hosts do not run the block above. The authoritative, per-harness
launcher for this one command is check_transport in the scripts map of
pr-review-config.yaml: the copilot entry runs it under PowerShell and
resolves the interpreter (python3, then py -3, then python), because a
Windows host may expose only the launcher. Read it from the map for the harness
you are on rather than transcribing a second copy here, which would drift.
Transport: gh (exit 0). Use the scripts below. This is CI and a normal
developer machine.
Transport: gh_unusable (exit 0). gh cannot reach GitHub here. Route the
work through the GitHub MCP operations and do not re-try the scripts to
confirm. The verdict names only what was measured: confirm the operations you
need are exposed before relying on them. Tool spelling differs by harness
(mcp__github__<op> in Claude Code, github/<op> in Copilot CLI).
- Exit 3 or 4. Not a transport problem: a quota window or a fixable token.
Script-to-tool mapping, the operations with no MCP equivalent, and the rules
for reporting one: references/transport-routing.md.
A transport failure is an unknown, never a verdict. Do not report a PR as
blocked, red, or unmergeable because the API was unreachable.
Decision Tree
Which transport? → check_github_transport.py (run once, before the rest)
Need GitHub data?
├─ List PRs (filtered) → get_pull_requests.py
├─ PR info/diff → get_pr_context.py
├─ CI check status → get_pr_checks.py
├─ Failing check also red on main? → triage_red_check.py
├─ CI failure logs → get_pr_check_logs.py
├─ Review comments → get_pr_review_comments.py
├─ Review threads → get_pr_review_threads.py
├─ Review verdicts (approved / changes requested) → get_pr_reviews.py
├─ Unique reviewers → get_pr_reviewers.py
├─ Unaddressed bot comments → get_unaddressed_comments.py
├─ PR merged check → test_pr_merged.py
├─ Copilot follow-up PRs → detect_copilot_followup_pr.py
├─ Validate PR description → validate_pr_description.py
├─ Issue info → get_issue_context.py
├─ Merge readiness check → test_pr_merge_ready.py
├─ PR reports blocked/dirty and cause is unclear → why_pr_blocked.py
├─ Latest milestone → get_latest_semantic_milestone.py
├─ Actionable backlog → get_actionable_items.py
└─ Need to take action?
├─ Create issue → new_issue.py
├─ Create PR → new_pr.py
├─ Reply to review → post_pr_comment_reply.py
├─ Reply to thread (GraphQL) → add_pr_review_thread_reply.py
├─ Comment on issue → post_issue_comment.py
├─ Add reaction → add_comment_reaction.py
├─ Apply labels → set_issue_labels.py
├─ Set issue milestone → set_issue_milestone.py
├─ Set PR/issue milestone (auto-detect) → set_item_milestone.py
├─ Assign issue → set_issue_assignee.py
├─ Resolve threads → resolve_pr_review_thread.py
├─ Unresolve threads → unresolve_pr_review_thread.py
├─ Process AI triage → invoke_pr_comment_processing.py
├─ Assign Copilot → invoke_copilot_assignment.py
├─ Enable/disable auto-merge → set_pr_auto_merge.py
├─ Close PR → close_pr.py
└─ Merge PR → merge_pr.py
Offline Invocation
In a network-restricted sandbox (triage, PR status checks), run read-only PR
scripts under scripts/pr/ with bare python3, not uv run.
uv run <script> resolves the whole project environment first, which downloads
anthropic==0.116.0 (a core dependency in pyproject.toml) from PyPI and times
out with no network. The read-only PR scripts never import the anthropic SDK.
They import only github_core, which parses YAML with a vendored fallback when
PyYAML is absent (issue #1844), so no third-party import is required.
# No PyPI round trip. Runs read-only PR status offline.
# CLAUDE_PLUGIN_ROOT is set in a vendored install; falls back to .claude in-repo.
SCRIPTS_DIR="${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/github/scripts/pr"
python3 "$SCRIPTS_DIR/get_pull_requests.py" --state open
python3 "$SCRIPTS_DIR/get_pr_context.py" --pr <N>
python3 "$SCRIPTS_DIR/get_pr_checks.py" --pr <N>
Any python3 works: github_core falls back to a vendored YAML parser when
PyYAML is absent (issue #1844), confirmed by running these scripts with both
PyYAML and the anthropic SDK blocked. The interpreter from
scripts/bootstrap-vm.sh and a project .venv/bin/python3 both ship PyYAML for
the faster path. Do not use uv run for read-only triage offline; it forces the
resolve that fetches anthropic.
The regression guard tests/test_pr_scripts_offline.py asserts the read-only PR
scripts and github_core import with the anthropic SDK blocked.
Scripts
PR Operations (scripts/pr/)
| Script |
Purpose |
Key Parameters |
get_pull_requests.py |
List PRs with filters |
--state, --label, --author, --base, --head, --search, --limit |
get_pr_context.py |
PR metadata, diff, files |
--pull-request, --include-changed-files, --include-diff |
get_pr_checks.py |
CI check status, polling |
--pull-request, --wait, --timeout-seconds, --required-only, --output-format {json,human,auto} |
get_pr_check_logs.py |
Fetch logs from failing CI checks |
--pull-request, --max-lines, --context-lines |
triage_red_check.py |
CI-failure triage step 1: is a failing PR check also red on main's latest run? Exit 0 green on main, exit 1 red on main (EvidenceUrl cites the main run), exit 3 cannot determine (never reported as green) |
--check-name, --branch, --history-depth, --pull-request |
get_pr_review_comments.py |
Paginated review comments with stale detection |
--pull-request, --include-issue-comments, --detect-stale, --exclude-stale, --only-stale |
get_pr_review_threads.py |
Thread-level review data |
--pull-request, --unresolved-only |
get_pr_reviews.py |
Review submissions (verdict state and body) |
--pull-request, --state, --output-format {json,human,auto} |
get_pr_reviewers.py |
Enumerate unique reviewers |
--pull-request, --exclude-bots |
get_unaddressed_comments.py |
Bot comments needing attention |
--pull-request |
get_unresolved_review_threads.py |
Unresolved thread IDs |
--pull-request |
wait_for_unresolved_zero.py |
Settling gate: poll until unresolved count holds at zero across bot scans |
--pull-request, --interval-seconds, --max-wait-seconds, --strict-pagination |
test_pr_merged.py |
Check if PR is merged |
--pull-request |
detect_copilot_followup_pr.py |
Detect Copilot follow-up PRs |
--pr-number, --owner, --repo |
post_pr_comment_reply.py |
Thread-preserving replies |
--pull-request, --comment-id, --body |
add_pr_review_thread_reply.py |
Reply to thread by ID (GraphQL) |
--pull-request, --thread-id, --body, --resolve |
resolve_pr_review_thread.py |
Mark threads resolved |
--thread-id --expected-pull-request or --pull-request --all |
unresolve_pr_review_thread.py |
Mark threads unresolved |
--thread-id or --pull-request --all |
get_thread_by_id.py |
Get single thread by ID |
--thread-id |
get_thread_conversation_history.py |
Full thread comment history |
--thread-id, --include-minimized |
test_pr_merge_ready.py |
Check merge readiness |
--pull-request, --ignore-ci, --ignore-threads |
why_pr_blocked.py |
Diagnose a mergeable_state/mergeStateStatus: blocked PR: decomposes it into missing/failing/pending required checks, unresolved review threads, or merge conflicts, and says explicitly when none of those hold (the field can be stale) |
--pull-request, --base-branch, --output-format {json,human,auto} |
set_pr_auto_merge.py |
Enable/disable auto-merge |
--pull-request, --enable/--disable, --merge-method |
invoke_pr_comment_processing.py |
Process AI triage output |
--pr-number, --verdict, --findings-json |
new_pr.py |
Create PR with validation |
--title, --body, --base |
validate_pr_description.py |
Validate PR description |
--title, --body, --body-file, --fail-on-violation |
close_pr.py |
Close PR with comment |
--pull-request, --comment |
merge_pr.py |
Merge with strategy |
--pull-request, --strategy, --delete-branch, --auto |
Issue Operations (scripts/issue/)
| Script |
Purpose |
Key Parameters |
get_issue_context.py |
Issue metadata (no comments) |
--issue |
get_issue_comments.py |
Issue comment thread (discourse) |
--issue, --limit |
new_issue.py |
Create new issue |
--title, --body, --labels |
close_issue.py |
Close with optional comment (--verify-claims aborts on a cited commit/PR the remote disproves, exit 1, and separately on one it could not check, exit 3 or 4) |
--issue, --reason, --comment, --verify-claims |
reopen_issue.py |
Reopen with optional comment |
--issue, --comment |
set_issue_labels.py |
Apply labels (auto-create) |
--issue, --labels, --priority |
set_issue_milestone.py |
Assign milestone |
--issue, --milestone |
post_issue_comment.py |
Comments with idempotency |
--issue, --body, --marker |
invoke_copilot_assignment.py |
Synthesize context for Copilot |
--issue-number, --what-if |
set_issue_assignee.py |
Assign users to issues |
--issue, --assignees |
Milestone Operations (scripts/milestone/)
| Script |
Purpose |
Key Parameters |
get_latest_semantic_milestone.py |
Detect latest semantic version milestone |
--owner, --repo |
set_item_milestone.py |
Assign milestone to PR/issue (auto-detect) |
--item-type, --item-number, --milestone-title |
Reactions (scripts/reactions/)
| Script |
Purpose |
Key Parameters |
add_comment_reaction.py |
Add emoji reactions (batch support) |
--comment-id, --reaction, --comment-type, --pull-request |
Notifications (scripts/notifications/)
| Script |
Purpose |
Key Parameters |
get_actionable_items.py |
List actionable backlog (reviews, authored PRs, assigned issues) |
--owner, --repo, --limit |
Utilities (scripts/utils/)
| Script |
Purpose |
Key Parameters |
extract_github_context.py |
Extract issue/PR references from text |
--text, --require-pr, --require-issue |
check_github_transport.py |
Report whether this session should use gh or the MCP tools |
--output-format |
Workflow Testing (scripts/)
| Script |
Purpose |
Key Parameters |
test_workflow_locally.py |
Test GitHub Actions locally with act |
--workflow, --event, --job, --dry-run |
Output Format
All scripts output structured JSON wrapped in a standard envelope per ADR-051.
Success envelope:
{
"Success": true,
"Data": { "Number": 42, "Title": "..." },
"Error": null,
"Metadata": { "Script": "get_pr_checks.py", "Version": "1.0.0", "Timestamp": "..." }
}
Error envelope:
{
"Success": false,
"Data": null,
"Error": { "Message": "PR not found", "Code": 2, "Type": "NotFound" },
"Metadata": { "Script": "get_pr_checks.py", "Version": "1.0.0", "Timestamp": "..." }
}
Usage:
SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT:-.claude}/skills/github/scripts"
result=$(python3 "$SCRIPTS_DIR/pr/get_pr_context.py" --pull-request 50)
echo "$result" | jq '.Data'
Exit codes follow ADR-035: 0=success, 1=logic error, 2=config error, 3=external failure, 4=auth error.
Process
This skill provides a toolkit of Python scripts for GitHub operations. Use scripts directly or compose them into workflows.
Basic Usage:
- Identify the operation needed using the Decision Tree
- Find the corresponding script in the Script Reference
- Call the script with required parameters
- Parse the JSON output
Example Flow:
SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT:-.claude}/skills/github/scripts"
# Get PR context
python3 "$SCRIPTS_DIR/pr/get_pr_context.py" --pull-request 123
# Check CI status
python3 "$SCRIPTS_DIR/pr/get_pr_checks.py" --pull-request 123
# Add comment if needed
python3 "$SCRIPTS_DIR/pr/post_pr_comment_reply.py" --pull-request 123 --comment-id 456 --body "CI failures detected"
GitHub Keywords for Issue Linking
GitHub automatically links and closes issues when PRs use specific keywords in PR descriptions, commit messages, or PR comments.
Supported Keywords
| Keyword |
Variations |
Example |
| Closes |
close, closed |
Closes #123 |
| Fixes |
fix, fixed |
Fixes #456 |
| Resolves |
resolve, resolved |
Resolves #789 |
Usage Patterns
In PR Descriptions:
## Summary
This PR adds feature X.
Closes #123
Fixes #456
In Commit Messages:
feat: Add feature X
Implements the new feature as specified.
Closes #123
Best Practices:
- Use keywords in PR description for primary issue
- Use keywords in commit bodies for related issues
- One keyword per line for clarity
- Place keywords in dedicated section or at end of description
Anti-Patterns
| Avoid |
Why |
Instead |
Raw gh pr view commands |
No structured output |
Use get_pr_context.py |
Raw gh api for comments |
Doesn't preserve threading |
Use post_pr_comment_reply.py |
| Replying to thread expecting auto-resolve |
Replies DON'T auto-resolve threads |
Use resolve_pr_review_thread.py after reply |
| Inline issue creation |
Missing validation |
Use new_issue.py |
| Multiple individual reactions |
88% slower |
Use batch mode in add_comment_reaction.py |
| Hardcoding owner/repo |
Breaks in forks |
Let scripts infer from git remote |
| Ignoring exit codes |
Missing error handling |
Check exit codes per ADR-035 |
| Skipping idempotency markers |
Duplicate comments |
Use --marker parameter |
Raw gh notify or notifications API |
403 with app tokens |
Use get_actionable_items.py |
Retrying gh after a session refusal |
The refusal has no reset; retries only burn the budget |
Run the transport preflight, then use the GitHub MCP operations |
| Reporting a PR blocked because a call failed |
An unreachable API is an unknown, not a verdict |
Name the transport failure as the cause |
Treating mergeable_state/mergeStateStatus: blocked as self-explanatory (e.g. guessing "pending approval" without checking) |
It is a cached, frequently-stale field. why_pr_blocked.py decomposes it into required checks, review threads, and merge conflicts, but does not query reviewDecision or the required-approving-review count, so a repo that requires approvals can still be blocked on that even when the script reports no cause |
Run why_pr_blocked.py first. GitHub's "A conversation must be resolved before this pull request can be merged" message means unresolved review threads: resolve them (resolve_pr_review_thread.py). If the script reports no cause but the PR is still blocked, also check get_pr_context.py's review_decision field against the target branch's required-approving-review count before reporting nothing to act on |
See Also
| Document |
Content |
| examples.md |
Complete script examples |
| patterns.md |
Reusable workflow patterns |
| copilot-prompts.md |
Creating @copilot directives |
| copilot-synthesis-guide.md |
Copilot context synthesis |
| api-reference.md |
Exit codes, API endpoints, troubleshooting |
| transport-routing.md |
Picking gh or the MCP tools, and the script-to-tool map |
scripts/github_core/ |
Shared Python helper functions |
Verification
Before completing a GitHub operation:
1---2name: github3description: Execute GitHub operations (PRs, issues, milestones, labels, comments, merges) using Python scripts with structured output and error handling. Use when working with pull requests, issues, review comments, CI checks, or milestones instead of raw gh. Use when you say "create a PR", "add label to issue", or "check CI status". Do NOT use to read a pasted github.com URL (use github-url-intercept).4license: MIT5---6# GitHub Skill78Use these scripts instead of raw `gh` commands for consistent error handling and structured output.910---1112## Triggers1314| Phrase | Operation |15|--------|-----------|16| `create a PR` | new_pr.py |17| `respond to review comments` | post_pr_comment_reply.py |18| `check CI status` | get_pr_checks.py / get_pr_check_logs.py |19| `close issue` | close_issue.py |20| `add label to issue` | set_issue_labels.py |2122---2324## Transport Preflight2526`gh` can be installed, hold a token, and still be refused for a whole session27(agent sandboxes that proxy egress do this). Decide the transport once, before28the first GitHub call:2930```bash31# CLAUDE_PLUGIN_ROOT is set in a vendored install; falls back to .claude in-repo.32SCRIPTS_DIR="${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/github/scripts/utils"33python3 "$SCRIPTS_DIR/check_github_transport.py"34```3536Non-POSIX hosts do not run the block above. The authoritative, per-harness37launcher for this one command is `check_transport` in the `scripts` map of38`pr-review-config.yaml`: the `copilot` entry runs it under PowerShell and39resolves the interpreter (`python3`, then `py -3`, then `python`), because a40Windows host may expose only the launcher. Read it from the map for the harness41you are on rather than transcribing a second copy here, which would drift.4243- `Transport: gh` (exit 0). Use the scripts below. This is CI and a normal44 developer machine.45- `Transport: gh_unusable` (exit 0). `gh` cannot reach GitHub here. Route the46 work through the GitHub MCP operations and do not re-try the scripts to47 confirm. The verdict names only what was measured: confirm the operations you48 need are exposed before relying on them. Tool spelling differs by harness49 (`mcp__github__<op>` in Claude Code, `github/<op>` in Copilot CLI).50- Exit 3 or 4. Not a transport problem: a quota window or a fixable token.5152Script-to-tool mapping, the operations with no MCP equivalent, and the rules53for reporting one: `references/transport-routing.md`.5455A transport failure is an unknown, never a verdict. Do not report a PR as56blocked, red, or unmergeable because the API was unreachable.5758---5960## Decision Tree6162```text63Which transport? → check_github_transport.py (run once, before the rest)6465Need GitHub data?66├─ List PRs (filtered) → get_pull_requests.py67├─ PR info/diff → get_pr_context.py68├─ CI check status → get_pr_checks.py69├─ Failing check also red on main? → triage_red_check.py70├─ CI failure logs → get_pr_check_logs.py71├─ Review comments → get_pr_review_comments.py72├─ Review threads → get_pr_review_threads.py73├─ Review verdicts (approved / changes requested) → get_pr_reviews.py74├─ Unique reviewers → get_pr_reviewers.py75├─ Unaddressed bot comments → get_unaddressed_comments.py76├─ PR merged check → test_pr_merged.py77├─ Copilot follow-up PRs → detect_copilot_followup_pr.py78├─ Validate PR description → validate_pr_description.py79├─ Issue info → get_issue_context.py80├─ Merge readiness check → test_pr_merge_ready.py81├─ PR reports blocked/dirty and cause is unclear → why_pr_blocked.py82├─ Latest milestone → get_latest_semantic_milestone.py83├─ Actionable backlog → get_actionable_items.py84└─ Need to take action?85 ├─ Create issue → new_issue.py86 ├─ Create PR → new_pr.py87 ├─ Reply to review → post_pr_comment_reply.py88 ├─ Reply to thread (GraphQL) → add_pr_review_thread_reply.py89 ├─ Comment on issue → post_issue_comment.py90 ├─ Add reaction → add_comment_reaction.py91 ├─ Apply labels → set_issue_labels.py92 ├─ Set issue milestone → set_issue_milestone.py93 ├─ Set PR/issue milestone (auto-detect) → set_item_milestone.py94 ├─ Assign issue → set_issue_assignee.py95 ├─ Resolve threads → resolve_pr_review_thread.py96 ├─ Unresolve threads → unresolve_pr_review_thread.py97 ├─ Process AI triage → invoke_pr_comment_processing.py98 ├─ Assign Copilot → invoke_copilot_assignment.py99 ├─ Enable/disable auto-merge → set_pr_auto_merge.py100 ├─ Close PR → close_pr.py101 └─ Merge PR → merge_pr.py102```103104---105106## Offline Invocation107108In a network-restricted sandbox (triage, PR status checks), run read-only PR109scripts under `scripts/pr/` with bare `python3`, not `uv run`.110111`uv run <script>` resolves the whole project environment first, which downloads112`anthropic==0.116.0` (a core dependency in `pyproject.toml`) from PyPI and times113out with no network. The read-only PR scripts never import the anthropic SDK.114They import only `github_core`, which parses YAML with a vendored fallback when115PyYAML is absent (issue #1844), so no third-party import is required.116117```bash118# No PyPI round trip. Runs read-only PR status offline.119# CLAUDE_PLUGIN_ROOT is set in a vendored install; falls back to .claude in-repo.120SCRIPTS_DIR="${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/github/scripts/pr"121python3 "$SCRIPTS_DIR/get_pull_requests.py" --state open122python3 "$SCRIPTS_DIR/get_pr_context.py" --pr <N>123python3 "$SCRIPTS_DIR/get_pr_checks.py" --pr <N>124```125126Any `python3` works: `github_core` falls back to a vendored YAML parser when127PyYAML is absent (issue #1844), confirmed by running these scripts with both128PyYAML and the anthropic SDK blocked. The interpreter from129`scripts/bootstrap-vm.sh` and a project `.venv/bin/python3` both ship PyYAML for130the faster path. Do not use `uv run` for read-only triage offline; it forces the131resolve that fetches anthropic.132133The regression guard `tests/test_pr_scripts_offline.py` asserts the read-only PR134scripts and `github_core` import with the anthropic SDK blocked.135136---137138## Scripts139140### PR Operations (`scripts/pr/`)141142| Script | Purpose | Key Parameters |143|--------|---------|----------------|144| `get_pull_requests.py` | List PRs with filters | `--state`, `--label`, `--author`, `--base`, `--head`, `--search`, `--limit` |145| `get_pr_context.py` | PR metadata, diff, files | `--pull-request`, `--include-changed-files`, `--include-diff` |146| `get_pr_checks.py` | CI check status, polling | `--pull-request`, `--wait`, `--timeout-seconds`, `--required-only`, `--output-format {json,human,auto}` |147| `get_pr_check_logs.py` | Fetch logs from failing CI checks | `--pull-request`, `--max-lines`, `--context-lines` |148| `triage_red_check.py` | CI-failure triage step 1: is a failing PR check also red on main's latest run? Exit 0 green on main, exit 1 red on main (EvidenceUrl cites the main run), exit 3 cannot determine (never reported as green) | `--check-name`, `--branch`, `--history-depth`, `--pull-request` |149| `get_pr_review_comments.py` | Paginated review comments with stale detection | `--pull-request`, `--include-issue-comments`, `--detect-stale`, `--exclude-stale`, `--only-stale` |150| `get_pr_review_threads.py` | Thread-level review data | `--pull-request`, `--unresolved-only` |151| `get_pr_reviews.py` | Review submissions (verdict state and body) | `--pull-request`, `--state`, `--output-format {json,human,auto}` |152| `get_pr_reviewers.py` | Enumerate unique reviewers | `--pull-request`, `--exclude-bots` |153| `get_unaddressed_comments.py` | Bot comments needing attention | `--pull-request` |154| `get_unresolved_review_threads.py` | Unresolved thread IDs | `--pull-request` |155| `wait_for_unresolved_zero.py` | Settling gate: poll until unresolved count holds at zero across bot scans | `--pull-request`, `--interval-seconds`, `--max-wait-seconds`, `--strict-pagination` |156| `test_pr_merged.py` | Check if PR is merged | `--pull-request` |157| `detect_copilot_followup_pr.py` | Detect Copilot follow-up PRs | `--pr-number`, `--owner`, `--repo` |158| `post_pr_comment_reply.py` | Thread-preserving replies | `--pull-request`, `--comment-id`, `--body` |159| `add_pr_review_thread_reply.py` | Reply to thread by ID (GraphQL) | `--pull-request`, `--thread-id`, `--body`, `--resolve` |160| `resolve_pr_review_thread.py` | Mark threads resolved | `--thread-id --expected-pull-request` or `--pull-request --all` |161| `unresolve_pr_review_thread.py` | Mark threads unresolved | `--thread-id` or `--pull-request --all` |162| `get_thread_by_id.py` | Get single thread by ID | `--thread-id` |163| `get_thread_conversation_history.py` | Full thread comment history | `--thread-id`, `--include-minimized` |164| `test_pr_merge_ready.py` | Check merge readiness | `--pull-request`, `--ignore-ci`, `--ignore-threads` |165| `why_pr_blocked.py` | Diagnose a `mergeable_state`/`mergeStateStatus: blocked` PR: decomposes it into missing/failing/pending required checks, unresolved review threads, or merge conflicts, and says explicitly when none of those hold (the field can be stale) | `--pull-request`, `--base-branch`, `--output-format {json,human,auto}` |166| `set_pr_auto_merge.py` | Enable/disable auto-merge | `--pull-request`, `--enable`/`--disable`, `--merge-method` |167| `invoke_pr_comment_processing.py` | Process AI triage output | `--pr-number`, `--verdict`, `--findings-json` |168| `new_pr.py` | Create PR with validation | `--title`, `--body`, `--base` |169| `validate_pr_description.py` | Validate PR description | `--title`, `--body`, `--body-file`, `--fail-on-violation` |170| `close_pr.py` | Close PR with comment | `--pull-request`, `--comment` |171| `merge_pr.py` | Merge with strategy | `--pull-request`, `--strategy`, `--delete-branch`, `--auto` |172173### Issue Operations (`scripts/issue/`)174175| Script | Purpose | Key Parameters |176|--------|---------|----------------|177| `get_issue_context.py` | Issue metadata (no comments) | `--issue` |178| `get_issue_comments.py` | Issue comment thread (discourse) | `--issue`, `--limit` |179| `new_issue.py` | Create new issue | `--title`, `--body`, `--labels` |180| `close_issue.py` | Close with optional comment (`--verify-claims` aborts on a cited commit/PR the remote disproves, exit 1, and separately on one it could not check, exit 3 or 4) | `--issue`, `--reason`, `--comment`, `--verify-claims` |181| `reopen_issue.py` | Reopen with optional comment | `--issue`, `--comment` |182| `set_issue_labels.py` | Apply labels (auto-create) | `--issue`, `--labels`, `--priority` |183| `set_issue_milestone.py` | Assign milestone | `--issue`, `--milestone` |184| `post_issue_comment.py` | Comments with idempotency | `--issue`, `--body`, `--marker` |185| `invoke_copilot_assignment.py` | Synthesize context for Copilot | `--issue-number`, `--what-if` |186| `set_issue_assignee.py` | Assign users to issues | `--issue`, `--assignees` |187188### Milestone Operations (`scripts/milestone/`)189190| Script | Purpose | Key Parameters |191|--------|---------|----------------|192| `get_latest_semantic_milestone.py` | Detect latest semantic version milestone | `--owner`, `--repo` |193| `set_item_milestone.py` | Assign milestone to PR/issue (auto-detect) | `--item-type`, `--item-number`, `--milestone-title` |194195### Reactions (`scripts/reactions/`)196197| Script | Purpose | Key Parameters |198|--------|---------|----------------|199| `add_comment_reaction.py` | Add emoji reactions (batch support) | `--comment-id`, `--reaction`, `--comment-type`, `--pull-request` |200201### Notifications (`scripts/notifications/`)202203| Script | Purpose | Key Parameters |204|--------|---------|----------------|205| `get_actionable_items.py` | List actionable backlog (reviews, authored PRs, assigned issues) | `--owner`, `--repo`, `--limit` |206207### Utilities (`scripts/utils/`)208209| Script | Purpose | Key Parameters |210|--------|---------|----------------|211| `extract_github_context.py` | Extract issue/PR references from text | `--text`, `--require-pr`, `--require-issue` |212| `check_github_transport.py` | Report whether this session should use gh or the MCP tools | `--output-format` |213214### Workflow Testing (`scripts/`)215216| Script | Purpose | Key Parameters |217|--------|---------|----------------|218| `test_workflow_locally.py` | Test GitHub Actions locally with act | `--workflow`, `--event`, `--job`, `--dry-run` |219220---221222## Output Format223224All scripts output structured JSON wrapped in a standard envelope per ADR-051.225226**Success envelope:**227228```json229{230 "Success": true,231 "Data": { "Number": 42, "Title": "..." },232 "Error": null,233 "Metadata": { "Script": "get_pr_checks.py", "Version": "1.0.0", "Timestamp": "..." }234}235```236237**Error envelope:**238239```json240{241 "Success": false,242 "Data": null,243 "Error": { "Message": "PR not found", "Code": 2, "Type": "NotFound" },244 "Metadata": { "Script": "get_pr_checks.py", "Version": "1.0.0", "Timestamp": "..." }245}246```247248**Usage:**249250```bash251SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT:-.claude}/skills/github/scripts"252result=$(python3 "$SCRIPTS_DIR/pr/get_pr_context.py" --pull-request 50)253echo "$result" | jq '.Data'254```255256Exit codes follow ADR-035: 0=success, 1=logic error, 2=config error, 3=external failure, 4=auth error.257258---259260## Process261262This skill provides a toolkit of Python scripts for GitHub operations. Use scripts directly or compose them into workflows.263264**Basic Usage:**2652661. Identify the operation needed using the Decision Tree2672. Find the corresponding script in the Script Reference2683. Call the script with required parameters2694. Parse the JSON output270271**Example Flow:**272273```bash274SCRIPTS_DIR="${CLAUDE_PLUGIN_ROOT:-.claude}/skills/github/scripts"275276# Get PR context277python3 "$SCRIPTS_DIR/pr/get_pr_context.py" --pull-request 123278279# Check CI status280python3 "$SCRIPTS_DIR/pr/get_pr_checks.py" --pull-request 123281282# Add comment if needed283python3 "$SCRIPTS_DIR/pr/post_pr_comment_reply.py" --pull-request 123 --comment-id 456 --body "CI failures detected"284```285286---287288## GitHub Keywords for Issue Linking289290GitHub automatically links and closes issues when PRs use specific keywords in PR descriptions, commit messages, or PR comments.291292### Supported Keywords293294| Keyword | Variations | Example |295|---------|-----------|---------|296| Closes | close, closed | `Closes #123` |297| Fixes | fix, fixed | `Fixes #456` |298| Resolves | resolve, resolved | `Resolves #789` |299300### Usage Patterns301302**In PR Descriptions:**303304```markdown305## Summary306This PR adds feature X.307308Closes #123309Fixes #456310```311312**In Commit Messages:**313314```text315feat: Add feature X316317Implements the new feature as specified.318319Closes #123320```321322**Best Practices:**323324- Use keywords in PR description for primary issue325- Use keywords in commit bodies for related issues326- One keyword per line for clarity327- Place keywords in dedicated section or at end of description328329---330331## Anti-Patterns332333| Avoid | Why | Instead |334|-------|-----|---------|335| Raw `gh pr view` commands | No structured output | Use `get_pr_context.py` |336| Raw `gh api` for comments | Doesn't preserve threading | Use `post_pr_comment_reply.py` |337| Replying to thread expecting auto-resolve | Replies DON'T auto-resolve threads | Use `resolve_pr_review_thread.py` after reply |338| Inline issue creation | Missing validation | Use `new_issue.py` |339| Multiple individual reactions | 88% slower | Use batch mode in `add_comment_reaction.py` |340| Hardcoding owner/repo | Breaks in forks | Let scripts infer from `git remote` |341| Ignoring exit codes | Missing error handling | Check exit codes per ADR-035 |342| Skipping idempotency markers | Duplicate comments | Use `--marker` parameter |343| Raw `gh notify` or notifications API | 403 with app tokens | Use `get_actionable_items.py` |344| Retrying `gh` after a session refusal | The refusal has no reset; retries only burn the budget | Run the transport preflight, then use the GitHub MCP operations |345| Reporting a PR blocked because a call failed | An unreachable API is an unknown, not a verdict | Name the transport failure as the cause |346| Treating `mergeable_state`/`mergeStateStatus: blocked` as self-explanatory (e.g. guessing "pending approval" without checking) | It is a cached, frequently-stale field. `why_pr_blocked.py` decomposes it into required checks, review threads, and merge conflicts, but does not query `reviewDecision` or the required-approving-review count, so a repo that requires approvals can still be blocked on that even when the script reports no cause | Run `why_pr_blocked.py` first. GitHub's "A conversation must be resolved before this pull request can be merged" message means unresolved review threads: resolve them (`resolve_pr_review_thread.py`). If the script reports no cause but the PR is still blocked, also check `get_pr_context.py`'s `review_decision` field against the target branch's required-approving-review count before reporting nothing to act on |347348---349350## See Also351352| Document | Content |353|----------|---------|354| [examples.md](references/examples.md) | Complete script examples |355| [patterns.md](references/patterns.md) | Reusable workflow patterns |356| [copilot-prompts.md](references/copilot-prompts.md) | Creating @copilot directives |357| [copilot-synthesis-guide.md](references/copilot-synthesis-guide.md) | Copilot context synthesis |358| [api-reference.md](references/api-reference.md) | Exit codes, API endpoints, troubleshooting |359| [transport-routing.md](references/transport-routing.md) | Picking gh or the MCP tools, and the script-to-tool map |360| `scripts/github_core/` | Shared Python helper functions |361362---363364## Verification365366Before completing a GitHub operation:367368- [ ] Correct script selected from Decision Tree369- [ ] Required parameters provided (PR/issue number)370- [ ] Response JSON parsed successfully371- [ ] Exit code is 0 (success)372- [ ] State change verified (for mutating operations)