Two Windows agent-loop friction fixes: 1. tools/mcp_tool.py (#56536): shutil.which(cmd, path=env_path) reads executable extensions from the PARENT process PATHEXT, not the MCP subprocess env — a stdio MCP config supplying both PATH and PATHEXT could fail to resolve a command its own env can locate, and startup then got a bare command name. On Windows, when the first which() call misses and the config env carries PATHEXT (any key casing), retry the resolution with the config's PATHEXT temporarily applied. 2. skills/ + optional-skills/ (#50606): 42 SKILL.md files that declare platforms: [.., windows] used python3 in their command examples. python3 does not exist on native Windows (the toolchain probe in the system prompt reports python3=missing), so every copy-pasted example burned a failed agent turn before self-correction. Replaced the command word python3 -> python (python3-config / python3.x version strings untouched). python is the spelling that exists in every Hermes-managed environment (Windows native, uv-managed venvs on all three OSes); agents on POSIX hosts additionally see the probed toolchain line and adapt either way.
371 lines
9.1 KiB
Markdown
371 lines
9.1 KiB
Markdown
---
|
|
name: github-issues
|
|
description: "Create, triage, label, assign GitHub issues via gh or REST."
|
|
version: 1.1.0
|
|
author: Hermes Agent
|
|
license: MIT
|
|
platforms: [linux, macos, windows]
|
|
metadata:
|
|
hermes:
|
|
tags: [GitHub, Issues, Project-Management, Bug-Tracking, Triage]
|
|
related_skills: [github-auth, github-pr-workflow]
|
|
---
|
|
|
|
# GitHub Issues Management
|
|
|
|
Create, search, triage, and manage GitHub issues. Each section shows `gh` first, then the `curl` fallback.
|
|
|
|
## Prerequisites
|
|
|
|
- Authenticated with GitHub (see `github-auth` skill)
|
|
- Inside a git repo with a GitHub remote, or specify the repo explicitly
|
|
|
|
### Setup
|
|
|
|
```bash
|
|
if command -v gh &>/dev/null && gh auth status &>/dev/null; then
|
|
AUTH="gh"
|
|
else
|
|
AUTH="git"
|
|
if [ -z "$GITHUB_TOKEN" ]; then
|
|
if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then
|
|
GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
|
|
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
|
|
GITHUB_TOKEN=$(uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
REMOTE_URL=$(git remote get-url origin)
|
|
OWNER_REPO=$(echo "$REMOTE_URL" | sed -E 's|.*github\.com[:/]||; s|\.git$||')
|
|
OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
|
|
REPO=$(echo "$OWNER_REPO" | cut -d/ -f2)
|
|
```
|
|
|
|
---
|
|
|
|
## 1. Viewing Issues
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
gh issue list
|
|
gh issue list --state open --label "bug"
|
|
gh issue list --assignee @me
|
|
gh issue list --search "authentication error" --state all
|
|
gh issue view 42
|
|
```
|
|
|
|
**With curl:**
|
|
|
|
```bash
|
|
# List open issues
|
|
curl -s \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&per_page=20" \
|
|
| python -c "
|
|
import sys, json
|
|
for i in json.load(sys.stdin):
|
|
if 'pull_request' not in i: # GitHub API returns PRs in /issues too
|
|
labels = ', '.join(l['name'] for l in i['labels'])
|
|
print(f\"#{i['number']:5} {i['state']:6} {labels:30} {i['title']}\")"
|
|
|
|
# Filter by label
|
|
curl -s \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&labels=bug&per_page=20" \
|
|
| python -c "
|
|
import sys, json
|
|
for i in json.load(sys.stdin):
|
|
if 'pull_request' not in i:
|
|
print(f\"#{i['number']} {i['title']}\")"
|
|
|
|
# View a specific issue
|
|
curl -s \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
|
|
| python -c "
|
|
import sys, json
|
|
i = json.load(sys.stdin)
|
|
labels = ', '.join(l['name'] for l in i['labels'])
|
|
assignees = ', '.join(a['login'] for a in i['assignees'])
|
|
print(f\"#{i['number']}: {i['title']}\")
|
|
print(f\"State: {i['state']} Labels: {labels} Assignees: {assignees}\")
|
|
print(f\"Author: {i['user']['login']} Created: {i['created_at']}\")
|
|
print(f\"\n{i['body']}\")"
|
|
|
|
# Search issues
|
|
curl -s \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
"https://api.github.com/search/issues?q=authentication+error+repo:$OWNER/$REPO" \
|
|
| python -c "
|
|
import sys, json
|
|
for i in json.load(sys.stdin)['items']:
|
|
print(f\"#{i['number']} {i['state']:6} {i['title']}\")"
|
|
```
|
|
|
|
## 2. Creating Issues
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
gh issue create \
|
|
--title "Login redirect ignores ?next= parameter" \
|
|
--body "## Description
|
|
After logging in, users always land on /dashboard.
|
|
|
|
## Steps to Reproduce
|
|
1. Navigate to /settings while logged out
|
|
2. Get redirected to /login?next=/settings
|
|
3. Log in
|
|
4. Actual: redirected to /dashboard (should go to /settings)
|
|
|
|
## Expected Behavior
|
|
Respect the ?next= query parameter." \
|
|
--label "bug,backend" \
|
|
--assignee "username"
|
|
```
|
|
|
|
**With curl:**
|
|
|
|
```bash
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues \
|
|
-d '{
|
|
"title": "Login redirect ignores ?next= parameter",
|
|
"body": "## Description\nAfter logging in, users always land on /dashboard.\n\n## Steps to Reproduce\n1. Navigate to /settings while logged out\n2. Get redirected to /login?next=/settings\n3. Log in\n4. Actual: redirected to /dashboard\n\n## Expected Behavior\nRespect the ?next= query parameter.",
|
|
"labels": ["bug", "backend"],
|
|
"assignees": ["username"]
|
|
}'
|
|
```
|
|
|
|
### Bug Report Template
|
|
|
|
```
|
|
## Bug Description
|
|
<What's happening>
|
|
|
|
## Steps to Reproduce
|
|
1. <step>
|
|
2. <step>
|
|
|
|
## Expected Behavior
|
|
<What should happen>
|
|
|
|
## Actual Behavior
|
|
<What actually happens>
|
|
|
|
## Environment
|
|
- OS: <os>
|
|
- Version: <version>
|
|
```
|
|
|
|
### Feature Request Template
|
|
|
|
```
|
|
## Feature Description
|
|
<What you want>
|
|
|
|
## Motivation
|
|
<Why this would be useful>
|
|
|
|
## Proposed Solution
|
|
<How it could work>
|
|
|
|
## Alternatives Considered
|
|
<Other approaches>
|
|
```
|
|
|
|
## 3. Managing Issues
|
|
|
|
### Add/Remove Labels
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
gh issue edit 42 --add-label "priority:high,bug"
|
|
gh issue edit 42 --remove-label "needs-triage"
|
|
```
|
|
|
|
**With curl:**
|
|
|
|
```bash
|
|
# Add labels
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/42/labels \
|
|
-d '{"labels": ["priority:high", "bug"]}'
|
|
|
|
# Remove a label
|
|
curl -s -X DELETE \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/42/labels/needs-triage
|
|
|
|
# List available labels in the repo
|
|
curl -s \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/labels \
|
|
| python -c "
|
|
import sys, json
|
|
for l in json.load(sys.stdin):
|
|
print(f\" {l['name']:30} {l.get('description', '')}\")"
|
|
```
|
|
|
|
### Assignment
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
gh issue edit 42 --add-assignee username
|
|
gh issue edit 42 --add-assignee @me
|
|
```
|
|
|
|
**With curl:**
|
|
|
|
```bash
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/42/assignees \
|
|
-d '{"assignees": ["username"]}'
|
|
```
|
|
|
|
### Commenting
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
gh issue comment 42 --body "Investigated — root cause is in auth middleware. Working on a fix."
|
|
```
|
|
|
|
**With curl:**
|
|
|
|
```bash
|
|
curl -s -X POST \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/42/comments \
|
|
-d '{"body": "Investigated — root cause is in auth middleware. Working on a fix."}'
|
|
```
|
|
|
|
### Closing and Reopening
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
gh issue close 42
|
|
gh issue close 42 --reason "not planned"
|
|
gh issue reopen 42
|
|
```
|
|
|
|
**With curl:**
|
|
|
|
```bash
|
|
# Close
|
|
curl -s -X PATCH \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
|
|
-d '{"state": "closed", "state_reason": "completed"}'
|
|
|
|
# Reopen
|
|
curl -s -X PATCH \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
|
|
-d '{"state": "open"}'
|
|
```
|
|
|
|
### Linking Issues to PRs
|
|
|
|
Issues are automatically closed when a PR merges with the right keywords in the body:
|
|
|
|
```
|
|
Closes #42
|
|
Fixes #42
|
|
Resolves #42
|
|
```
|
|
|
|
To create a branch from an issue:
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
gh issue develop 42 --checkout
|
|
```
|
|
|
|
**With git (manual equivalent):**
|
|
|
|
```bash
|
|
git checkout main && git pull origin main
|
|
git checkout -b fix/issue-42-login-redirect
|
|
```
|
|
|
|
## 4. Issue Triage Workflow
|
|
|
|
When asked to triage issues:
|
|
|
|
1. **List untriaged issues:**
|
|
|
|
```bash
|
|
# With gh
|
|
gh issue list --label "needs-triage" --state open
|
|
|
|
# With curl
|
|
curl -s \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=needs-triage&state=open" \
|
|
| python -c "
|
|
import sys, json
|
|
for i in json.load(sys.stdin):
|
|
if 'pull_request' not in i:
|
|
print(f\"#{i['number']} {i['title']}\")"
|
|
```
|
|
|
|
2. **Read and categorize** each issue (view details, understand the bug/feature)
|
|
|
|
3. **Apply labels and priority** (see Managing Issues above)
|
|
|
|
4. **Assign** if the owner is clear
|
|
|
|
5. **Comment with triage notes** if needed
|
|
|
|
## 5. Bulk Operations
|
|
|
|
For batch operations, combine API calls with shell scripting:
|
|
|
|
**With gh:**
|
|
|
|
```bash
|
|
# Close all issues with a specific label
|
|
gh issue list --label "wontfix" --json number --jq '.[].number' | \
|
|
xargs -I {} gh issue close {} --reason "not planned"
|
|
```
|
|
|
|
**With curl:**
|
|
|
|
```bash
|
|
# List issue numbers with a label, then close each
|
|
curl -s \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=wontfix&state=open" \
|
|
| python -c "import sys,json; [print(i['number']) for i in json.load(sys.stdin)]" \
|
|
| while read num; do
|
|
curl -s -X PATCH \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
https://api.github.com/repos/$OWNER/$REPO/issues/$num \
|
|
-d '{"state": "closed", "state_reason": "not_planned"}'
|
|
echo "Closed #$num"
|
|
done
|
|
```
|
|
|
|
## Quick Reference Table
|
|
|
|
| Action | gh | curl endpoint |
|
|
|--------|-----|--------------|
|
|
| List issues | `gh issue list` | `GET /repos/{o}/{r}/issues` |
|
|
| View issue | `gh issue view N` | `GET /repos/{o}/{r}/issues/N` |
|
|
| Create issue | `gh issue create ...` | `POST /repos/{o}/{r}/issues` |
|
|
| Add labels | `gh issue edit N --add-label ...` | `POST /repos/{o}/{r}/issues/N/labels` |
|
|
| Assign | `gh issue edit N --add-assignee ...` | `POST /repos/{o}/{r}/issues/N/assignees` |
|
|
| Comment | `gh issue comment N --body ...` | `POST /repos/{o}/{r}/issues/N/comments` |
|
|
| Close | `gh issue close N` | `PATCH /repos/{o}/{r}/issues/N` |
|
|
| Search | `gh issue list --search "..."` | `GET /search/issues?q=...` |
|