GitHub Skill

SkillAI & models

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).

Then ask your AI: use the GitHub Skill

Details

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add it as a step in a pipeline you build on Ahel, or connect Ahel to Claude, ChatGPT, Cursor, Claude Code or Codex and ask your AI to use it.

What this skill tells your AI

The instructions your AI receives, as published by rjmurillo/ai-agents in .claude/skills/github/SKILL.md and read by Ahel’s review.

Use these scripts instead of raw gh commands for consistent error handling and structured output.


Triggers

PhraseOperation
create a PRnew_pr.py
respond to review commentspost_pr_comment_reply.py
check CI statusget_pr_checks.py / get_pr_check_logs.py
close issueclose_issue.py
add label to issueset_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
├─ Issue parent, sub-issues, blockers, related → get_issue_relationships.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
   ├─ Link issues (parent, sub-issue, blocked-by, blocking, relates-to) → set_issue_relationship.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
   ├─ Update PR branch with its base (server side) → update_pr_branch.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/)

ScriptPurposeKey Parameters
get_pull_requests.pyList PRs with filters--state, --label, --author, --base, --head, --search, --limit
get_pr_context.pyPR metadata, diff, files--pull-request, --include-changed-files, --include-diff
get_pr_checks.pyCI check status, polling--pull-request, --wait, --timeout-seconds, --required-only, --output-format {json,human,auto}
get_pr_check_logs.pyFetch logs from failing CI checks--pull-request, --max-lines, --context-lines
triage_red_check.pyCI-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.pyPaginated review comments with stale detection--pull-request, --include-issue-comments, --detect-stale, --exclude-stale, --only-stale
get_pr_review_threads.pyThread-level review data--pull-request, --unresolved-only
get_pr_reviews.pyReview submissions (verdict state and body)--pull-request, --state, --output-format {json,human,auto}
get_pr_reviewers.pyEnumerate unique reviewers--pull-request, --exclude-bots
get_unaddressed_comments.pyBot comments needing attention--pull-request
get_unresolved_review_threads.pyUnresolved thread IDs--pull-request
wait_for_unresolved_zero.pySettling gate: poll until unresolved count holds at zero across bot scans--pull-request, --interval-seconds, --max-wait-seconds, --strict-pagination
test_pr_merged.pyCheck if PR is merged--pull-request
detect_copilot_followup_pr.pyDetect Copilot follow-up PRs--pr-number, --owner, --repo
post_pr_comment_reply.pyThread-preserving replies--pull-request, --comment-id, --body
add_pr_review_thread_reply.pyReply to thread by ID (GraphQL)--pull-request, --thread-id, --body, --resolve
resolve_pr_review_thread.pyMark threads resolved--thread-id --expected-pull-request or --pull-request --all
unresolve_pr_review_thread.pyMark threads unresolved--thread-id or --pull-request --all
get_thread_by_id.pyGet single thread by ID--thread-id
get_thread_conversation_history.pyFull thread comment history--thread-id, --include-minimized
test_pr_merge_ready.pyCheck merge readiness--pull-request, --ignore-ci, --ignore-threads
why_pr_blocked.pyDiagnose 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.pyEnable/disable auto-merge--pull-request, --enable/--disable, --merge-method
invoke_pr_comment_processing.pyProcess AI triage output--pr-number, --verdict, --findings-json
new_pr.pyCreate PR with validation--title, --body, --base
validate_pr_description.pyValidate PR description--title, --body, --body-file, --fail-on-violation
update_pr_branch.pyServer-side "Update branch": merges the base into the PR head. Exit 0 when the update is requested, completed, or already up to date (already_up_to_date: true); exit 1 for a PR that is not open, or a head that differs from --expected-head-sha (reason: head_moved); exit 2 when the PR is not found; exit 3 for another API failure or a --wait timeout (the wait ends only when compare shows the PR is not behind); exit 4 for an auth failure. Unattended callers should pass --expected-head-sha--pull-request, --expected-head-sha, --wait, --timeout-seconds
close_pr.pyClose PR with comment--pull-request, --comment
merge_pr.pyMerge pinned to the reviewed head, then read back and audited (ADR-112)--pull-request, --expected-head-sha, --strategy, --delete-branch, --auto
audit_closing_claims.pyFleet audit of open-PR closing claims: extracts closing keywords from PR bodies, commit messages, and auto-merge overrides; classifies Markdown context (body) and plain-text context (commits/overrides); flags a claim unsupported when it can reach the eventual squash commit without a matching active body claim (exit 1). Needs a token with administration read: reachability depends on the repository's squash_merge_commit_message setting, which GitHub omits for anyone else, and the audit exits 3 rather than reporting a clean fleet it cannot verify--state open, --artifact, --resume-from, --output-format {json,human,auto}
edit_pr_body.pyEdit a PR body with a SHA-256 stale-write guard--pull-request, --body/--body-file, --expected-hash, --dry-run

Issue Operations (scripts/issue/)

ScriptPurposeKey Parameters
get_issue_context.pyIssue metadata (no comments)--issue
get_issue_comments.pyIssue comment thread (discourse)--issue, --limit
new_issue.pyCreate new issue; --source required, agent needs Step 0--title, --body, --labels, --source, --blocked-by, --signal, --milestone
close_issue.pyClose 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.pyReopen with optional comment--issue, --comment
set_issue_labels.pyApply labels (auto-create)--issue, --labels, --priority
set_issue_milestone.pyAssign milestone--issue, --milestone
post_issue_comment.pyComments with idempotency--issue, --body, --marker
invoke_copilot_assignment.pySynthesize context for Copilot--issue-number, --what-if
set_issue_assignee.pyAssign users to issues--issue, --assignees
get_issue_relationships.pyNative parent, sub-issues, blocked-by, blocking, relates-to--issue
set_issue_relationship.pyAdd or remove a native issue link (idempotent)--issue, --relation, --target, --remove, --replace-parent, --dry-run

Milestone Operations (scripts/milestone/)

ScriptPurposeKey Parameters
get_latest_semantic_milestone.pyDetect latest semantic version milestone--owner, --repo
set_item_milestone.pyAssign milestone to PR/issue (auto-detect)--item-type, --item-number, --milestone-title

Repository Settings (repo/ scripts)

ScriptPurposeKey Parameters
manage_squash_merge_message.pyRead or update the squash_merge_commit_message repository setting with an expected-current-value guard; reports a real before/after re-read. Needs a token with administration read: GitHub omits merge settings for anyone else, and the script exits 3 rather than assuming a value--set, --expected-current, --dry-run, --output-format {json,human,auto}

Reactions (scripts/reactions/)

ScriptPurposeKey Parameters
add_comment_reaction.pyAdd emoji reactions (batch support)--comment-id, --reaction, --comment-type, --pull-request

Notifications (scripts/notifications/)

ScriptPurposeKey Parameters
get_actionable_items.pyList actionable backlog (reviews, authored PRs, assigned issues)--owner, --repo, --limit

Utilities (scripts/utils/)

ScriptPurposeKey Parameters
extract_github_context.pyExtract issue/PR references from text--text, --require-pr, --require-issue
check_github_transport.pyReport whether this session should use gh or the MCP tools--output-format

Workflow Testing (scripts/)

ScriptPurposeKey Parameters
test_workflow_locally.pyTest 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:

  1. Identify the operation needed using the Decision Tree
  2. Find the corresponding script in the Script Reference
  3. Call the script with required parameters
  4. 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

KeywordVariationsExample
Closesclose, closedCloses #123
Fixesfix, fixedFixes #456
Resolvesresolve, resolvedResolves #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

Linking Issues to Each Other

A #123 in an issue body is text only. It sets no parent, no dependency, and no relation. Whenever an issue body names another issue as a parent, child, blocker, or context, add the native link with set_issue_relationship.py.

The text says--relation
"Parent", "child of #N", "part of epic #N", "umbrella #N"parent
An epic lists its childrensub-issue
"Blocked by #N", "depends on #N", or an epic's execution steps name #Nblocked-by
"Blocks #N", "unblocks #N"blocking
Problem statement, context, "split out of #N", "follow-up to #N"relates-to

After new_issue.py, link the new issue in the next call. Pull requests cannot be linked this way; use a closing keyword. Full decision rules, the audit procedure, and the MCP fallback: references/issue-relationships.md.

Issue Provenance

new_issue.py requires --source, which records who selected the work:

  • human: an explicit human request selected this issue. An owner login, a human-started session, or a user approving publication does not qualify.
  • agent: anything an agent chose. It also needs Step 0 evidence: --blocked-by (who is blocked, and on what) and --signal (the metric, log, run, or ticket that proves it). A body that already carries a ## Step 0 block with ### Q3 and ### Q5 supplies the evidence instead; passing both is an error.

Blank answers, canonical hedge phrases, and conflicting source:* labels exit 2 before any GitHub call. The script creates the source:human or source:agent label when the repository lacks it, and passes it to gh issue create itself, so a label failure creates no issue. The script redacts flag evidence before it publishes it. It refuses a body Step 0 block that carries a secret, because it publishes the body unchanged.

--source human also appends <!-- source:human --> as the last body line. The script rejects that marker in any other body.

The script's check covers this script only. The GitHub MCP issue_write tool, raw gh issue create, and workflow steps that call the API bypass it. The Label Issue Source workflow labels those issues after they open: bots and owner-login issues without a trailing human marker get source:agent, a burst of owner issues within ten minutes forces source:agent, and every other author gets source:human. It labels only. It does not close or block.


Anti-Patterns

AvoidWhyInstead
Raw gh pr view commandsNo structured outputUse get_pr_context.py
Raw gh api for commentsDoesn't preserve threadingUse post_pr_comment_reply.py
Replying to thread expecting auto-resolveReplies DON'T auto-resolve threadsUse resolve_pr_review_thread.py after reply
Inline issue creationMissing validationUse new_issue.py
"Blocked by #N" or "Parent: #N" in a body with no native linkGitHub shows no dependency, parent, or progressAdd the link with set_issue_relationship.py
Raw gh api graphql for addSubIssue, addBlockedBy, addRelatesToNo idempotency, no PR or parent-conflict guardUse set_issue_relationship.py
Multiple individual reactions88% slowerUse batch mode in add_comment_reaction.py
Hardcoding owner/repoBreaks in forksLet scripts infer from git remote
Ignoring exit codesMissing error handlingCheck exit codes per ADR-035
Skipping idempotency markersDuplicate commentsUse --marker parameter
Raw gh notify or notifications API403 with app tokensUse get_actionable_items.py
Retrying gh after a session refusalThe refusal has no reset; retries only burn the budgetRun the transport preflight, then use the GitHub MCP operations
Reporting a PR blocked because a call failedAn unreachable API is an unknown, not a verdictName 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 causeRun 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

DocumentContent
examples.mdComplete script examples
patterns.mdReusable workflow patterns
copilot-prompts.mdCreating @copilot directives
copilot-synthesis-guide.mdCopilot context synthesis
api-reference.mdExit codes, API endpoints, troubleshooting
transport-routing.mdPicking gh or the MCP tools, and the script-to-tool map
issue-relationships.mdChoosing parent, sub-issue, blocked-by, blocking, or relates-to
scripts/github_core/Shared Python helper functions

Verification

Before completing a GitHub operation:

  • Correct script selected from Decision Tree
  • Required parameters provided (PR/issue number)
  • Response JSON parsed successfully
  • Exit code is 0 (success)
  • State change verified (for mutating operations)

Signals

GitHub stars
47
Forks
14
Last commit
Oct 2026
Hacker News mentions
20

Ahel review

  • S4info
    community integration, published by rjmurillo, not github
  • K6low
    bundled executables the agent is told to run

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
github-rjmurillo
Source
github.com/rjmurillo/ai-agents