Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions .claude/skills/gh-issues/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
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 label `foo`.
- `--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:

```bash
# 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 `assignees` array 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 `--label` filter if given.
- Apply `--limit` if 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:

```bash
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:

1. Re-check assignment state (someone else may have grabbed it):
```bash
gh issue view <num> --json assignees -q '.assignees[].login'
me=$(gh api user -q .login)
```
- Empty output β†’ unassigned, proceed.
- Only `$me` listed β†’ already mine, proceed (skip self-assign step).
- Any other login present β†’ skip this issue.

2. Invoke the `/gh-issue` skill 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.md` entry under `## Unreleased` for user-facing changes
- commit with conventional message + `Closes #<num>` in the body
- PR opened via `/pr #<num>`

3. **Before pushing**, run the exact command CI's strict job uses β€” plain
sequential runs miss simple-mode and strict-mode bugs that CI rejects:
```bash
./bashunit --parallel --simple --strict tests/
```
In `--simple` mode `print_line` emits only a one-char marker, so assert on
pure helper functions, not on `print_line` stdout. Under `--strict`
(`set -euo pipefail`): no `[ cond ] && assignment` (use `if`), initialise
arrays with `=()` and read elements as `${arr[i]:-}`, and never enable
`shopt -s extdebug` in the parent shell (isolate it inside a `$()` subshell).

4. After `/gh-issue` returns, wait for CI green on the PR:
```bash
gh pr checks --watch
```
Fix 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`.

5. Merge when allowed:
```bash
gh pr merge --auto --squash --admin
```

6. **Close the issue if the squash-merge did not.** GitHub builds the squash commit from the **PR body**, and `/pr` writes `Related #<num>` there (never `Closes`), so the `Closes #<num>` in the branch commit body is lost. After merge:
```bash
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 `sed` for the tick β€” bash `${body/- [ ] #N/...}` treats
`[ ]` as a glob (it matches a space, not literal brackets) and silently
does nothing:
```bash
gh issue view <tracker> --json body -q .body \
| sed 's/- \[ \] #<num>/- [x] #<num>/' | gh issue edit <tracker> --body-file -
```

7. Sync `main` for next iteration:
```bash
git checkout main && git fetch origin main && git reset --hard origin/main
```

8. Continue with next issue.

## Stop Conditions

Halt the loop and surface the failure when:

- `/gh-issue` errors 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) or `make lint` (EditorConfig) fails.
- CI stays red after one fix attempt.
- Merge is blocked by branch protection beyond `--admin` bypass.
- `--limit` reached.
- 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

- `gh` authenticated, can read issues, open and merge PRs.
- Worktree clean.
- `main` exists and tracks `origin/main`.
- `/gh-issue` and `/pr` skills available in this session.

## Notes

- **Bash 3.0+ compatibility is mandatory.** No `declare -A`, `[[ ]]`, `${var,,}`, negative array indexing, or `&>>` in `src/`. See `.claude/rules/bash-style.md`.
- **Quality gate is `make sa` + `make lint`, not bare `shfmt`.** `shfmt -w .` without project flags rewrites the whole tree (collapses line-continuations, flips binary-op style, tabs the `indent_size = unset` files). Match the surrounding 2-space style by hand and rely on `make lint` (EditorConfig) to verify. There is no `shfmt` make target.
- **Know your baseline.** `tests/unit/coverage_subshell_test.sh` fails 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.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ indent_style = tab
[{tests/acceptance/**.sh,src/console_header.sh,docs/command-line.md}]
indent_size = unset

[.claude/**.md]
indent_size = unset

[src/coverage.sh]
max_line_length = unset
Loading