Skip to content

feat(skills): add github-issue-triage skill#20

Open
blarghmatey wants to merge 2 commits into
mainfrom
feat/github-issue-triage
Open

feat(skills): add github-issue-triage skill#20
blarghmatey wants to merge 2 commits into
mainfrom
feat/github-issue-triage

Conversation

@blarghmatey

Copy link
Copy Markdown
Member

What are the relevant tickets?

N/A

Description (What does it do?)

Adds a new process/github-issue-triage skill for auditing open GitHub issues to identify which are outdated, already completed, or superseded by newer issues.

The skill codifies the enumerate → batch → fan-out → synthesize pattern: fetch all open issues, group them by theme, dispatch parallel subagents to cross-reference each batch against the live codebase and git history, then synthesize the results into a tiered close/keep report.

New files:

  • skills/process/github-issue-triage/SKILL.md — main skill definition covering all five phases, the verdict rubric (LIKELY_OUTDATED / POSSIBLY_OUTDATED / STILL_RELEVANT), and common patterns (technology replaced, superseded epic, active feature branch, bot-maintained issue)
  • scripts/fetch-issues.sh — downloads all open issues from a GitHub repo to a JSON file via gh issue list
  • scripts/explore-issue.sh — cross-references a single issue against a local git repo: derives keywords from the title, runs git log, rg code search, remote branch search, and closed PR lookup
  • scripts/close-issues.sh — bulk-closes or bulk-comments on a list of issue numbers; defaults to --dry-run; supports per-group ISSUE_TRIAGE_REASON override
  • references/verdict-examples.md — annotated examples from a real triage run (59 issues, mitodl/ol-infrastructure) showing what evidence supports each verdict tier

The skill was developed and validated against an actual triage run that identified 14 strong close candidates and 2 trivial quick-win code changes out of 59 open issues.

How can this be tested?

  1. Clone a repo with a moderate number of open GitHub issues.
  2. Run fetch-issues.sh <owner/repo> — verify it produces a valid JSON array.
  3. Pick a single issue number and run explore-issue.sh <number> <repo-path> issues_full.json — verify it prints commits, code references, and branch hits.
  4. Run echo "9999" | close-issues.sh --dry-run <owner/repo> — verify it prints the dry-run line without touching GitHub.
  5. Invoke the skill in Claude Code on a repo with known stale issues and confirm the parallel agent output matches the rubric in the skill.

Additional Context

The explore-issue.sh script derives search keywords automatically from the issue title (lowercase words ≥5 chars) so it requires no manual configuration per issue. The close-issues.sh script uses gh issue close --reason completed which maps to GitHub's "completed" close reason, keeping the close distinct from "not planned."

Adds a process skill for auditing open GitHub issues to identify stale,
completed, or superseded items via parallel subagent codebase
cross-referencing.

Includes:
- SKILL.md with enumerate→batch→fan-out→synthesize protocol
- scripts/fetch-issues.sh: download all open issues to JSON
- scripts/explore-issue.sh: cross-reference a single issue against a repo
- scripts/close-issues.sh: bulk-close or bulk-comment with --dry-run guard
- references/verdict-examples.md: annotated LIKELY/POSSIBLY/STILL examples

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new github-issue-triage skill under the process category, which includes documentation and three bash scripts (fetch-issues.sh, explore-issue.sh, and close-issues.sh) to automate auditing and triaging open GitHub issues. The review feedback highlights several critical robustness improvements for the scripts, such as preventing script crashes from SIGPIPE errors under set -o pipefail when using head, improving keyword extraction logic to handle short or missing terms, using printf instead of echo for portable newline expansion in comments, and handling carriage returns or trailing whitespace in input issue lists.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +29 to +30
BODY=$(jq -r --argjson n "${ISSUE_NUM}" \
'.[] | select(.number == $n) | .body' "${ISSUES_JSON}" | head -c 600)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using head -c 600 in a pipeline under set -o pipefail will cause the script to crash with a SIGPIPE error (exit code 141) if the issue body is longer than 600 characters. This is because head exits early after reading 600 bytes, closing the pipe while jq is still writing.

We can avoid this completely and improve performance by truncating the body directly within jq using string slicing.

Suggested change
BODY=$(jq -r --argjson n "${ISSUE_NUM}" \
'.[] | select(.number == $n) | .body' "${ISSUES_JSON}" | head -c 600)
BODY=$(jq -r --argjson n "${ISSUE_NUM}" \\
'.[] | select(.number == $n) | (.body // "") | .[0:600]' "${ISSUES_JSON}")

Comment on lines +38 to +43
# Derive search keywords from the title (lower-case words >=5 chars)
KEYWORDS=$(echo "${TITLE}" | tr '[:upper:]' '[:lower:]' \
| grep -oE '[a-z]{5,}' | sort -u | head -6 | tr '\n' '|' | sed 's/|$//')

echo "--- Keywords extracted: ${KEYWORDS} ---"
echo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two issues with the current keyword extraction logic:

  1. If the issue title has no words of 5 or more characters (e.g., "Fix UI"), KEYWORDS will be empty. An empty KEYWORDS pattern causes subsequent git log --grep="" and grep -iE "" commands to match every commit and branch, flooding the output with irrelevant results.
  2. Restricting keywords to 5 or more characters and only alphabetic characters ([a-z]{5,}) discards highly specific technical terms and acronyms (e.g., auth, cors, saml, ipv6, imds, v2).

We can lower the minimum length to 4, allow alphanumeric characters, and handle the empty KEYWORDS case gracefully in subsequent searches.

Suggested change
# Derive search keywords from the title (lower-case words >=5 chars)
KEYWORDS=$(echo "${TITLE}" | tr '[:upper:]' '[:lower:]' \
| grep -oE '[a-z]{5,}' | sort -u | head -6 | tr '\n' '|' | sed 's/|$//')
echo "--- Keywords extracted: ${KEYWORDS} ---"
echo
# Derive search keywords from the title (lower-case alphanumeric words >=4 chars)
KEYWORDS=$(echo "${TITLE}" | tr '[:upper:]' '[:lower:]' \\
| grep -oE '[a-z0-9]{4,}' | sort -u | head -6 | tr '\\n' '|' | sed 's/|$//')
echo "--- Keywords extracted: ${KEYWORDS:-None} ---"
echo

Comment on lines +59 to +62
echo "--- All branches mentioning keywords ---"
git -C "${REPO_PATH}" branch -r 2>/dev/null \
| grep -iE "${KEYWORDS}" | head -10 || echo " (none)"
echo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Under set -o pipefail, if git branch -r returns more than 10 matching branches, head -10 will exit early and cause grep to fail with a SIGPIPE (exit code 141). This makes the entire pipeline fail, which triggers the || echo " (none)" fallback. As a result, instead of showing the first 10 branches, the script will incorrectly print (none).

We can fix this by capturing the output into a variable with || true inside the subshell to safely handle the pipe termination.

Suggested change
echo "--- All branches mentioning keywords ---"
git -C "${REPO_PATH}" branch -r 2>/dev/null \
| grep -iE "${KEYWORDS}" | head -10 || echo " (none)"
echo
echo "--- All branches mentioning keywords ---"
BRANCHES=$(git -C "${REPO_PATH}" branch -r 2>/dev/null | grep -iE "${KEYWORDS}" | head -10 || true)
if [[ -n "${BRANCHES}" ]]; then
echo "${BRANCHES}"
else
echo " (none)"
fi

Comment on lines +10 to +11
# echo "1749\n822\n407" | ./close-issues.sh --dry-run <owner/repo>
# echo "1749\n822\n407" | ./close-issues.sh --close <owner/repo>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In standard bash, echo "1749\n822\n407" does not expand \n by default. It will output the literal string 1749\n822\n407 on a single line, which will fail the numeric validation check in the script.

Using printf is much more portable and behaves consistently across different shells and platforms.

Suggested change
# echo "1749\n822\n407" | ./close-issues.sh --dry-run <owner/repo>
# echo "1749\n822\n407" | ./close-issues.sh --close <owner/repo>
# printf "1749\\n822\\n407\\n" | ./close-issues.sh --dry-run <owner/repo>
# printf "1749\\n822\\n407\\n" | ./close-issues.sh --close <owner/repo>

Comment on lines +25 to +27
while IFS= read -r ISSUE_NUM; do
[[ -z "${ISSUE_NUM}" ]] && continue
[[ "${ISSUE_NUM}" =~ ^[0-9]+$ ]] || { echo "SKIP: '${ISSUE_NUM}' is not a number" >&2; continue; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the input list of issue numbers contains trailing whitespace or carriage returns (common when files are saved with Windows/DOS line endings \r\n), the regex check [[ "${ISSUE_NUM}" =~ ^[0-9]+$ ]] will fail and skip valid issue numbers.

We can make the input processing more robust by stripping carriage returns and trimming whitespace using tr and xargs.

Suggested change
while IFS= read -r ISSUE_NUM; do
[[ -z "${ISSUE_NUM}" ]] && continue
[[ "${ISSUE_NUM}" =~ ^[0-9]+$ ]] || { echo "SKIP: '${ISSUE_NUM}' is not a number" >&2; continue; }
while IFS= read -r line; do
ISSUE_NUM=$(echo "${line}" | tr -d '\\r' | xargs)
[[ -z "${ISSUE_NUM}" ]] && continue
[[ "${ISSUE_NUM}" =~ ^[0-9]+$ ]] || { echo "SKIP: '${ISSUE_NUM}' is not a number" >&2; continue; }

…ath mapping

Some organisations use a catch-all issue tracker repo (e.g. mitodl/hq)
where product labels route each issue to the actual codebase to check.
Without handling this, explore-issue.sh would search the empty tracker
repo and produce no evidence, making all verdicts unreliable.

Changes:
- SKILL.md: adds repo mode table (direct vs tracker), Phase 1b to build
  label→path map from product label namespace, annotate issues with
  resolved_path, and updated agent prompt template instructing agents to
  search resolved_path rather than the tracker repo; adds 'tracker repo
  issue with no product label' common pattern
- explore-issue.sh: accepts optional 4th arg (label-map JSON); resolves
  the target repo from the issue's labels before searching; falls back to
  the provided repo-path with a warning when no label matches or the
  resolved directory doesn't exist; also falls back to repo root when
  src/ subdirectory is absent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new workflow skill (process/github-issue-triage) to help audit open GitHub issues for staleness/completion/supersession using an enumerate → batch → fan-out → synthesize approach, backed by helper scripts.

Changes:

  • Adds a new github-issue-triage skill definition with a phased triage workflow and verdict rubric.
  • Introduces three supporting shell scripts to fetch issues, explore a single issue against a repo, and bulk comment/close issues.
  • Updates skills indexes and adds example verdict write-ups from a real triage run.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
skills/README.md Adds the new skill to the top-level skills index.
skills/process/README.md Adds the new skill to the process skills index.
skills/process/github-issue-triage/SKILL.md Defines the triage workflow, phases, and command examples for agents/users.
skills/process/github-issue-triage/scripts/fetch-issues.sh Script to download open issues into a normalized JSON array.
skills/process/github-issue-triage/scripts/explore-issue.sh Script to cross-reference a single issue against local git/code/branches/PRs.
skills/process/github-issue-triage/scripts/close-issues.sh Script to bulk comment/close issues read from STDIN.
skills/process/github-issue-triage/references/verdict-examples.md Example evidence patterns and verdicts from a prior triage run.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +33 to +41
TITLE=$(jq -r --argjson n "${ISSUE_NUM}" \
'.[] | select(.number == $n) | .title' "${ISSUES_JSON}")
CREATED=$(jq -r --argjson n "${ISSUE_NUM}" \
'.[] | select(.number == $n) | .createdAt' "${ISSUES_JSON}")
BODY=$(jq -r --argjson n "${ISSUE_NUM}" \
'.[] | select(.number == $n) | .body' "${ISSUES_JSON}" | head -c 600)
LABELS=$(jq -r --argjson n "${ISSUE_NUM}" \
'[.[] | select(.number == $n) | .labels[]] | join(", ")' "${ISSUES_JSON}")

Comment on lines +80 to +83
echo "--- Recent commits mentioning these keywords ---"
git -C "${REPO_PATH}" log --oneline --since="${CREATED}" \
--grep="${KEYWORDS}" --regexp-ignore-case 2>/dev/null | head -15 || true
echo
Comment on lines +90 to +91
rg -r --include="*.py" --include="*.yaml" --include="*.yml" \
-l "${kw}" "${SRC_DIR}" 2>/dev/null | head -5 || true
Comment on lines +95 to +98
echo "--- Remote branches mentioning keywords ---"
git -C "${REPO_PATH}" branch -r 2>/dev/null \
| grep -iE "${KEYWORDS}" | head -10 || echo " (none)"
echo
Comment on lines +17 to +35
REPO="${1:?Usage: $0 <owner/repo> [outfile]}"
OUTFILE="${2:-./issues_full.json}"

echo "Fetching open issues from ${REPO}..." >&2

gh issue list \
--repo "${REPO}" \
--state open \
--limit 500 \
--json number,title,body,labels,createdAt,updatedAt \
| jq '[.[] | {
number,
title,
body,
labels: [.labels[].name],
createdAt: .createdAt[:10],
updatedAt: .updatedAt[:10]
}]' \
> "${OUTFILE}"
```bash
# Did the expected artifact get created?
ls <resolved_path>/src/<expected-path>/ 2>/dev/null
rg -r "<keyword>" <resolved_path>/src --include="*.py" -l
Comment on lines +232 to +233
rg -r "<old-tool>" <resolved_path>/src -l | head # expect empty
rg -r "<new-tool>" <resolved_path>/src -l | head # expect populated
# Close or comment on a list of GitHub issues from a triage report.
#
# Reads issue numbers from STDIN (one per line) and either:
# --dry-run prints the actions that would be taken (default)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants