| name | github-issues |
| description | Create, manage, triage, and close GitHub issues. Search existing issues, add labels, assign people, and link to PRs. Works with gh CLI or falls back to git + GitHub REST API via curl. |
| version | 1.1.0 |
| author | Hermes Agent |
| license | MIT |
| 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
Private repos behind Deckhand shims
When operating on a private repo whose git/gh binaries are wrapped by Deckhand scope shims, do not assume ordinary gh issue list --repo ... will work from every chat/session. First verify the session has the identity fields the shim needs (HERMES_SESSION_USER_ID, HERMES_SESSION_PLATFORM, HERMES_SESSION_CHAT_ID) and that the current chat is bound to a scope that authorizes the target repo. If gh fails closed with unknown operator or no active scope, report that as an authorization/context gap, not as “no issues.” For read-only status requests, fall back to durable repo-local handoff/issue-map documents only as clearly labeled secondary evidence, and keep direct GitHub issue state marked unverified until an authorized Deckhand/gh path succeeds. Avoid browser or unauthenticated REST as the primary path for private repos; 404 commonly means private/auth-blocked, not absent.
Setup
if command -v gh &>/dev/null && gh auth status &>/dev/null; then
AUTH="gh"
elif [ -n "${GITHUB_TOKEN:-}" ]; then
AUTH="curl"
else
echo "Authenticate with gh or export GITHUB_TOKEN explicitly before using curl fallbacks." >&2
exit 1
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)
Do not scrape tokens from local Hermes environment files, Git credential stores, or shell history in reusable issue scripts. Prefer gh authentication; use REST fallbacks only when GITHUB_TOKEN is already explicitly present in the environment.
1. Viewing Issues
With gh:
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:
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&per_page=20" \
| python3 -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']}\")"
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&labels=bug&per_page=20" \
| python3 -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i:
print(f\"#{i['number']} {i['title']}\")"
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
| python3 -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']}\")"
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/search/issues?q=authentication+error+repo:$OWNER/$REPO" \
| python3 -c "
import sys, json
for i in json.load(sys.stdin)['items']:
print(f\"#{i['number']} {i['state']:6} {i['title']}\")"
2. Creating Issues
Recommended pre-create workflow for feature issues
Before creating a new feature issue, do a quick grounding pass to avoid duplicates and align with repo taxonomy:
-
If the user's request is to convert notes into issues across multiple repos, first classify each candidate issue by canonical home before creating anything:
- client-sensitive engagement notes, follow-up wording, pilot selection, and knowledge-base governance → private/client wiki or engagement repo;
- engineering calculations, solver workflows, report-generation patterns, and model-readiness gaps → engineering/model repo;
- public/regional data, field economics, HSE/safety datasets, and portfolio screening → data repo.
Produce a routing summary with repo buckets before or alongside issue bodies so the user can audit placement.
-
Search existing issues by the key nouns/phrases in the request. Use broad searches first, then exact-title filtering if needed. gh --jq is not the standalone jq CLI and does not accept --arg; for exact title checks, pipe JSON to Python or jq yourself, for example:
TITLE='feat(area): concise issue title'
gh issue list --state all --limit 200 --json number,title,url \
| python3 -c 'import json,os,sys; title=os.environ["TITLE"]; [print(f"#{i[\"number\"]}\t{i[\"url\"]}") for i in json.load(sys.stdin) if i["title"] == title]'
If a duplicate-check command errors, stop and repair the check or verify by another broad search before creating issues; do not silently continue on a broken duplicate guard.
-
Inspect existing labels in the repo and reuse the closest category/priority labels.
-
If the feature touches an existing initiative, reference the parent/related issue numbers in the body.
-
If an existing open issue already substantially covers the requested feature, prefer updating that issue instead of creating a duplicate. Add a clarifying comment to expand scope, adjust labels if needed, and link any companion cadence/governance issue rather than opening a second overlapping feature ticket.
-
Prefer writing the body to a temp markdown file and using --body-file for long, structured issue descriptions.
-
If you are creating a linked issue tree (parent + children), render any placeholders like <PARENT_ISSUE> / <QUEUE_ISSUE> into temporary files before calling gh issue create. Do not rely on post-hoc mental substitution.
-
After creation or update, immediately verify the final artifact's title, labels, URL, and rendered body.
-
If GitHub issue access is blocked (for example, private repo token lacks repository.issues and gh issue list/create returns GraphQL: Resource not accessible by personal access token), do not stop with only a verbal plan. Preserve the work in a durable issue packet plus an executable creation script:
- write a repo-routed markdown packet containing every issue title, target repo, labels, body, and acceptance check;
- write a
scripts/create-...-issues.sh helper using gh issue create --repo <owner/repo> --body-file ...;
- use only labels you have verified exist, or fall back to broadly available labels such as
enhancement / documentation when label access cannot be audited;
- run
bash -n on the helper and count expected issue entries versus create_issue calls;
- report the exact GitHub access blocker and the command to run after access is fixed.
-
When the issue work also commits plan/report artifacts, verify both local and remote repository state before claiming closeout: pushed HEAD, local origin/main, remote refs/heads/main, ahead/behind count, and tracked worktree cleanliness. Do not treat a local commit hash alone as enough evidence.
-
If push emits GitHub-side ref-lock/cannot-lock-ref noise during closeout, do not assume success or failure from the warning. Re-query remote and tracking refs; only call it benign when HEAD == origin/<branch> == remote refs/heads/<branch> and ahead/behind is 0 0.
-
If ending a session with a linked issue tree, produce a restart-safe closeout: issue states/gate labels, artifact paths, validation results, pushed commits, remote-ref sync evidence, and preserved unrelated worktrees. See references/issue-tree-exit-closeout.md.
-
If you accidentally create an issue with unresolved placeholders, fix it immediately with gh issue edit --body-file ... and then re-verify the rendered body.
-
For layered architecture/governance issue trees, see references/layered-architecture-review-issue-tree.md; it includes parent/child issue structure, data/execution/report layer scope, and the rule that output/report residency must mirror input/data residency unless an explicit promotion gate says otherwise.
-
When researching a user-referenced incident from another chat/channel, distinguish the observed runtime/chat failure from adjacent GitHub asset/workflow issues. Use references/incident-to-issue-evidence-research.md to search session history, inspect candidate issue bodies/comments, extract evidence, and explicitly report any gap where no direct issue exists.
-
Before closing layered architecture contract implementation issues, run the closeout checks in references/layered-architecture-contract-closeout.md; in particular, schema readiness gates must reject placeholder checksums and any unresolved adversarial MAJOR blocks commit/close.
-
For per-machine repository placement decisions, create one decision issue per machine in the user's requested order and do not perform repo moves/deletes/sync changes inside issue creation. Use references/machine-repo-placement-decision-issues.md for the reusable issue body shape and verification checklist. Keep delegation/dispatch strategy independent from repo placement unless a separate approved delegation plan explicitly requires otherwise; repo placement should be justified by locality, canonical source of truth, storage footprint, sync/backup risk, licensed tooling, and machine role. When the first machine issue establishes the baseline, include a completion note/acceptance criterion that the story leaves a reusable pattern for consistent tier-1 repo folder structure, methodical primary/reference placement decisions, and repo harness/file ecosystem handling through a single repo-tracked authority.
-
For revisions to completed/closed work, prefer a new open revision issue with a parent link-back comment when the closed issue should remain the baseline trace. Use references/closed-issue-revision-thread.md for the body shape and verification checklist.
Example:
gh issue list --limit 50 --search "llm-wiki OR document intelligence OR resource intelligence OR standards"
gh label list
cat > /tmp/feature-issue.md <<'EOF'
<feature summary>
<why now>
- <item>
- <item>
- <artifact>
- <artifact>
- Parent:
- Related:
EOF
gh issue create \
--title "feat(area): concise issue title" \
--body-file /tmp/feature-issue.md \
--label enhancement \
--label priority:medium
gh issue view <new-number> --json number,title,url,labels,body
With gh:
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:
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
Plan-review closeout for gated work
When an issue-scoped plan has been written and adversarial-reviewed, close the planning gate transactionally before reporting success:
- Commit and push the plan plus durable review artifacts first.
- Post a GitHub issue comment with plan path, review artifact paths, verdict summary, validation commands, and pushed commit hash.
- Move labels from the planning intake state to
status:plan-review.
- Re-query the issue and verify the expected label and comment URL.
- State explicitly that
status:plan-review is not implementation approval; implementation remains blocked until the user applies status:plan-approved.
If adversarial reviewers return MAJOR findings but the user explicitly asked to post/re-post the plans for their review, it is still valid to label the issues status:plan-review after the reviews complete. Do not soften the verdicts. The issue comment and plan summary must say that the plans are awaiting user decision, are not approved, and that implementation remains blocked until explicit user approval.
For multi-issue re-review batches, use one consistent issue comment shape across all issues: review command/date, provider list, per-provider artifact paths and verdicts, consolidated disagreement artifact, gate state, and pushed commit hash. This makes the user-review queue scannable and prevents one issue from silently drifting from the others.
Use --body-file for the comment body so paths, backticks, and verdict tables do not trigger shell quoting issues.
Removing a child issue from an active issue tree
When the user removes a machine/workstream/deliverable from scope, do not only close the child issue. Keep the parent and child synchronized:
- Update the parent issue body first: remove the child from the active checklist and add an explicit
Out of scope / removed note preserving the child issue number and rationale.
- Post a child closeout comment before closing, using the race-safe pattern: result, user direction/evidence, actions taken, residual scope, and how to reintroduce the work later.
- Close the child with
--reason "not planned".
- Replace planning labels with terminal labels where the repo taxonomy supports it, e.g. remove
status:needs-plan, add status:closed and wontfix.
- Comment on the parent with a concise scope-update note.
- Verify both sides: child
stateReason: NOT_PLANNED, closeout comment present, parent active checklist no longer includes the child, and parent body preserves the out-of-scope rationale.
See references/issue-tree-scope-removal.md for reusable comment templates and verification criteria.
User approval reconciliation for gated plans
When the user explicitly approves a status:plan-review issue but includes correction notes, treat approval as a reconciliation transaction, not a bare label flip:
- Update the plan artifact first so the user's corrections are durable in the implementation contract.
- Commit and push the plan update before changing gate labels.
- Post an issue comment recording the exact approval notes, plan path, and commit hash.
- Only after that, replace
status:plan-review with status:plan-approved.
- Re-query the issue and verify title, URL, open state, and final labels before reporting success.
- Keep the user-correction notes scoped to the implementation plan; do not silently mutate repository layout, move data, or delete aliases unless the approved plan explicitly includes that execution step.
This is the one valid case where the agent may apply status:plan-approved: the user has explicitly approved the plan in chat or via an equivalent review signal. Never infer approval from reviewer APPROVE verdicts alone.
Add/Remove Labels
With gh:
gh issue edit 42 --add-label "priority:high,bug"
gh issue edit 42 --remove-label "needs-triage"
With curl:
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42/labels \
-d '{"labels": ["priority:high", "bug"]}'
curl -s -X DELETE \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42/labels/needs-triage
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/labels \
| python3 -c "
import sys, json
for l in json.load(sys.stdin):
print(f\" {l['name']:30} {l.get('description', '')}\")"
Assignment
With gh:
gh issue edit 42 --add-assignee username
gh issue edit 42 --add-assignee @me
With curl:
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42/assignees \
-d '{"assignees": ["username"]}'
Commenting
Interactive clarification threads
When an issue is being used as a live review/clarification thread, keep the GitHub discussion actionable and correction-friendly:
- Post user corrections back to the issue as concise decision bullets, not as a long narrative.
- When the user asks for unresolved blockers, post one consolidated comment with only the open confirmations/blockers; do not mix in already-settled decisions except as short context.
- End interpretation comments with an explicit correction invitation, e.g. "Please correct if any part of this interpretation is wrong."
- Prefer issue comments for engineering review clarifications so the user can edit/respond inline and the implementation plan has a durable trace.
- Separate defaults from ranges/comparison cases. Example: "default = 3.08 kn; plots may show 0–4 kn; 4 kn is range upper bound, not default."
With gh:
gh issue comment 42 --body "Investigated — root cause is in auth middleware. Working on a fix."
For multiline comments or bodies containing backticks / code spans, prefer a body file to avoid shell command-substitution and quoting problems:
cat > /tmp/issue-comment.md <<'EOF'
Quick triage update:
- `scheduler_config.yml` is missing the job entry
- recommend adding test coverage for registration/config loading
EOF
gh issue comment 42 --body-file /tmp/issue-comment.md
If gh issue comment returns a transient GitHub 5xx/504 timeout, treat success as unknown rather than blindly resubmitting. First query recent comments for a unique marker from the body; only retry if the marker is absent.
MARKER='unique marker from your comment'
if ! gh issue view 42 --comments --json comments --jq '.comments[-10:][].body' | grep -F "$MARKER" >/dev/null; then
gh issue comment 42 --body-file /tmp/issue-comment.md
fi
With curl:
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:
gh issue close 42
gh issue close 42 --reason "not planned"
gh issue reopen 42
With curl:
curl -s -X PATCH \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
-d '{"state": "closed", "state_reason": "completed"}'
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:
gh issue develop 42 --checkout
With git (manual equivalent):
git checkout main && git pull origin main
git checkout -b fix/issue-42-login-redirect
4. Issue Triage Workflow
When asked to triage issues:
- List untriaged issues:
gh issue list --label "needs-triage" --state open
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=needs-triage&state=open" \
| python3 -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i:
print(f\"#{i['number']} {i['title']}\")"
-
Read and categorize each issue (view details, understand the bug/feature)
-
Apply labels and priority (see Managing Issues above)
-
Assign if the owner is clear
-
Comment with triage notes if needed
5. Bulk Operations
For batch operations, combine API calls with shell scripting:
With gh:
gh issue list --label "wontfix" --json number --jq '.[].number' | \
xargs -I {} gh issue close {} --reason "not planned"
With curl:
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=wontfix&state=open" \
| python3 -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=... |