name: gh-issues description: Walk over all open GitHub issues that are unassigned or assigned to the current user, and process each one via the /gh-issue skill, sequentially. user-invocable: true argument-hint: "[--limit N] [--label foo] [--dry-run]" disable-model-invocation: true allowed-tools: Read, Bash, Skill
GitHub Issues Watcher
Purpose
Process every open GitHub issue that is unassigned or assigned to the current user (@me), one after another, by delegating each to the /gh-issue skill. Stop on first hard failure so it can be inspected.
Driven from inside the Claude session rather than a polling shell script.
Args
--limit N— process at most N issues this run (default: all).--label foo— only issues carrying labelfoo.--dry-run— list issues that would be processed; do not invoke/gh-issue.
Strip leading # if user passes #123 style.
Phase 1: Discover
Fetch open issues that are unassigned or assigned to @me, oldest first. GitHub search does not OR these cleanly, so run two queries and merge:
# Unassigned
gh issue list \
--state open \
--search "no:assignee" \
--json number,title,labels,assignees,createdAt \
--limit 200
# Assigned to me
gh issue list \
--state open \
--assignee "@me" \
--json number,title,labels,assignees,createdAt \
--limit 200
Merge:
- Deduplicate by
number. - Keep only issues whose
assigneesarray is empty or contains the current user (gh api user -q .login). - Drop issues assigned to anyone else (defensive).
- Skip tracker/epic issues — those whose body is a checklist of other issues (e.g.
- [ ] #123 …). Process the child issues directly, not the parent. - Apply
--labelfilter if given. - Apply
--limitif given. - Sort ascending by
createdAt(FIFO).
Print the queue: #<num> <title> [assignee] per line, where [assignee] is unassigned or @me. If empty, exit cleanly.
Phase 2: Worktree Sanity
Before touching any issue:
git status --porcelain
git fetch origin main
git checkout main && git reset --hard origin/main
Abort if worktree dirty. Never auto-stash.
Phase 3: Process Loop
For each issue in the queue:
Re-check assignment state (someone else may have grabbed it):
gh issue view <num> --json assignees -q '.assignees[].login' me=$(gh api user -q .login)- Empty output → unassigned, proceed.
- Only
$melisted → already mine, proceed (skip self-assign step). - Any other login present → skip this issue.
Invoke the
/gh-issueskill with the issue number. That skill owns:- self-assign via
gh issue edit <num> --add-assignee @me(no-op if already assigned) - branch from fresh
main(prefix from labels:fix/,feat/,docs/) - strict TDD: RED → GREEN → REFACTOR (no production code without a failing test first)
- full suite green locally (
./bashunit tests/and./bashunit --parallel tests/) CHANGELOG.mdentry under## Unreleasedfor user-facing changes- commit with conventional message +
Closes #<num>in the body - PR opened via
/pr #<num>
- self-assign via
Before pushing, run the exact command CI's strict job uses — plain sequential runs miss simple-mode and strict-mode bugs that CI rejects:
./bashunit --parallel --simple --strict tests/In
--simplemodeprint_lineemits only a one-char marker, so assert on pure helper functions, not onprint_linestdout. Under--strict(set -euo pipefail): no[ cond ] && assignment(useif), initialise arrays with=()and read elements as${arr[i]:-}, and never enableshopt -s extdebugin the parent shell (isolate it inside a$()subshell).After
/gh-issuereturns, wait for CI green on the PR:gh pr checks --watchFix red checks on the branch before moving on. Bash 3.0 CI fails often — check that job specifically. A red
docker/registry step is usually a transient Docker Hub timeout, not your code:gh run rerun <id> --failed.Merge when allowed:
gh pr merge --auto --squash --adminClose the issue if the squash-merge did not. GitHub builds the squash commit from the PR body, and
/prwritesRelated #<num>there (neverCloses), so theCloses #<num>in the branch commit body is lost. After merge:gh issue view <num> --json state -q .state # still OPEN? gh issue close <num> --reason completed -c "Done in #<pr> (merged)."If the issue belongs to a tracker/epic, tick its checkbox in the parent issue body. Use
sedfor the tick — bash${body/- [ ] #N/...}treats[ ]as a glob (it matches a space, not literal brackets) and silently does nothing:gh issue view <tracker> --json body -q .body \ | sed 's/- \[ \] #<num>/- [x] #<num>/' | gh issue edit <tracker> --body-file -Sync
mainfor next iteration:git checkout main && git fetch origin main && git reset --hard origin/mainContinue with next issue.
Stop Conditions
Halt the loop and surface the failure when:
/gh-issueerrors out or leaves the worktree dirty../bashunit tests/or./bashunit --parallel tests/fails after implementation (for the right reason — distinguish pre-existing failures; see Notes).make sa(ShellCheck) ormake lint(EditorConfig) fails.- CI stays red after one fix attempt.
- Merge is blocked by branch protection beyond
--adminbypass. --limitreached.- Queue empty.
Do not retry blindly. Report which issue failed and why.
Dry Run
With --dry-run, only execute Phase 1 and print the queue. No assignment, no branching, no commits.
Preconditions
ghauthenticated, can read issues, open and merge PRs.- Worktree clean.
mainexists and tracksorigin/main./gh-issueand/prskills available in this session.
Notes
- Bash 3.0+ compatibility is mandatory. No
declare -A,[[ ]],${var,,}, negative array indexing, or&>>insrc/. See.claude/rules/bash-style.md. - Quality gate is
make sa+make lint, not bareshfmt.shfmt -w .without project flags rewrites the whole tree (collapses line-continuations, flips binary-op style, tabs theindent_size = unsetfiles). Match the surrounding 2-space style by hand and rely onmake lint(EditorConfig) to verify. There is noshfmtmake target. - Know your baseline.
tests/unit/coverage_subshell_test.shfails on some macOS setups regardless of the change — confirm a failure is new (git stash+ re-run) before treating it as a regression. - Treat GitHub CI as the full quality gate; locally run focused tests during implementation, the full sequential + parallel suite once before commit.
- Never split bundled changes into multiple PRs unless the issue explicitly demands it.