.claude-plugin/skills/preflight-closing-issues-fix/SKILL.md
# Skill: preflight-closing-issues-fix ## Overview | Field | Value | |-------|-------| | Date | 2026-02-21 | | Issue | #802 | | PR | #912 | | Category | tooling | | Objective | Fix `preflight_check.sh` Check 3 false positives caused by free-text PR search matching issue numbers in unrelated PR titles/bodies | | Outcome | Success — 6 bash tests pass, all pre-commit hooks green, PR created with auto-merge | ## When to Use Trigger this skill when: - A preflight/guard script uses `gh pr list --s
npx skillsauth add homericintelligence/projectscylla .claude-plugin/skills/preflight-closing-issues-fixInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Field | Value |
|-------|-------|
| Date | 2026-02-21 |
| Issue | #802 |
| PR | #912 |
| Category | tooling |
| Objective | Fix preflight_check.sh Check 3 false positives caused by free-text PR search matching issue numbers in unrelated PR titles/bodies |
| Outcome | Success — 6 bash tests pass, all pre-commit hooks green, PR created with auto-merge |
Trigger this skill when:
gh pr list --search "<number>" to detect if a PR already covers an issueclosingIssuesReferencesTrigger phrases:
gh pr list --search "$ISSUE" is a full-text search — it matches any PR whose title or body contains the string "735". This causes false positives whenever a PR description casually references the issue number without formally closing it.
GitHub's closingIssuesReferences field is populated only when a PR body contains a recognized closing keyword (Closes #N, Fixes #N, Resolves #N) or the issue is explicitly linked via the GitHub UI. It is the authoritative signal for "this PR closes this issue."
grep -n "gh pr list --search" scripts/preflight_check.sh
# Expected: gh pr list --search "$ISSUE" --state all --json ...
Before (false-positive prone):
PR_JSON=$(gh pr list --search "$ISSUE" --state all --json number,title,state 2>/dev/null)
MERGED_PRS=$(echo "$PR_JSON" | jq -r '.[] | select(.state == "MERGED") | "\(.number): \(.title)"')
OPEN_PRS=$(echo "$PR_JSON" | jq -r '.[] | select(.state == "OPEN") | "\(.number): \(.title)"')
After (precise, uses closingIssuesReferences):
CANDIDATE_JSON=$(gh pr list --state all --json number,title,state --limit 100 2>/dev/null)
MERGED_PRS=""
OPEN_PRS=""
while IFS=$'\t' read -r pr_num pr_title pr_state; do
[[ -z "$pr_num" ]] && continue
CLOSES=$(gh pr view "$pr_num" --json closingIssuesReferences \
--jq '.closingIssuesReferences[].number' 2>/dev/null)
if echo "$CLOSES" | grep -qx "$ISSUE"; then
if [[ "$pr_state" == "MERGED" ]]; then
MERGED_PRS+="${pr_num}: ${pr_title}"$'\n'
elif [[ "$pr_state" == "OPEN" ]]; then
OPEN_PRS+="${pr_num}: ${pr_title}"$'\n'
fi
fi
done < <(echo "$CANDIDATE_JSON" | jq -r '.[] | [.number,.title,.state] | @tsv')
MERGED_PRS="${MERGED_PRS%$'\n'}"
OPEN_PRS="${OPEN_PRS%$'\n'}"
gh functionsKey technique: mock gh as a bash function in a subshell, capturing exit code with a temp file (not a pipe, which loses $?):
run_preflight_with_exit() {
local issue="$1"
local mock_body="$2"
local tmpfile
tmpfile=$(mktemp)
bash -c "
${mock_body}
export -f gh
bash '${PREFLIGHT}' '${issue}' 2>&1
" > "$tmpfile" 2>&1
LAST_EXIT=$?
LAST_OUTPUT=$(strip_ansi "$(cat "$tmpfile")")
rm -f "$tmpfile"
}
| Test | Scenario | Expected |
|------|----------|----------|
| 1 | No PRs exist | PASS exit 0 |
| 2 | MERGED PR, closingRef=[issue] | STOP exit 1 |
| 3 | OPEN PR, closingRef=[issue] | WARN exit 0 |
| 4 | MERGED PR mentioning issue in title, empty closingRef | PASS exit 0 (regression) |
| 5 | Multiple PRs, only one with closingRef | STOP with only that PR listed |
| 6 | PRs exist but closingRef targets different issue | PASS exit 0 |
ShellCheck flags sed 's/\x1b...' with SC2001. Since \x1b hex escape cannot be expressed in bash parameter expansion, suppress it with an inline directive:
# SC2001 is suppressed: bash parameter expansion cannot match \x1b hex escapes.
# shellcheck disable=SC2001
strip_ansi() { echo "$1" | sed 's/\x1b\[[0-9;]*m//g'; }
Update the Pre-Flight Check Results table rows for STOP/WARN to note that matching is now via closingIssuesReferences, not text search.
git add scripts/preflight_check.sh SKILL.md tests/test_preflight_check.sh
git commit -m "fix(preflight): use closingIssuesReferences for precise PR-issue matching
Closes #802"
git push -u origin <branch>
gh pr create --title "..." --body "Closes #802"
gh pr merge --auto --rebase <pr-number>
Skill tool denied: Attempted commit-commands:commit-push-pr skill but it was blocked by don't ask mode. Fell back to direct Bash git commands — this is the correct fallback and works identically.
Pipe loses exit code: Initial attempt captured output=$(run_check3 ... | strip_ansi) — piping through sed consumed the subshell exit code, making all exit-code assertions fail. Fix: write to a temp file, then LAST_EXIT=$? before stripping colors.
| File | Change |
|------|--------|
| tests/claude-code/shared/skills/github/gh-implement-issue/scripts/preflight_check.sh | Replace Check 3 free-text search with two-phase closingIssuesReferences lookup |
| tests/claude-code/shared/skills/github/gh-implement-issue/SKILL.md | Update STOP/WARN table rows to note closingIssuesReferences matching |
| tests/claude-code/shared/skills/github/gh-implement-issue/tests/test_preflight_check.sh | New — 6 bash tests with mocked gh functions |
| Parameter | Value |
|-----------|-------|
| PR fetch limit | --limit 100 (avoids timeout on large repos) |
| closingIssuesReferences jq expression | .closingIssuesReferences[].number |
| grep for exact issue match | grep -qx "$ISSUE" (full-line match, avoids 73 matching 735) |
| Test runner | bash tests/test_preflight_check.sh |
| All pre-commit hooks | Pass (ShellCheck, Markdown lint, YAML lint, Ruff, mypy) |
gh pr list --search is full-text — it searches titles AND bodies, making it unsuitable for issue ownership checks.closingIssuesReferences is authoritative — populated only by recognized closing keywords or UI links.gh pr view calls where N = total PR count. The --limit 100 bound keeps it practical for most repos.gh as a function + export -f gh in a subshell; use temp files not pipes to preserve exit codes.[PASS] / [STOP] prefixes, strip ANSI before grep — otherwise color codes cause silent mismatches.development
# Skill: docs-status-fix ## Overview | Field | Value | |------------|----------------------------------------------------| | Date | 2026-02-19 | | Category | documentation | | Objective | Fix stale "Current Status" in CLAUDE.md | | Issue | #753 | | PR | #810
tools
# Preflight Check Skill Propagation ## Overview | Field | Value | |-------|-------| | Date | 2026-02-21 | | Issue | #803 | | Objective | Add preflight check to `worktree-create` skill so developers bypassing `gh-implement-issue` still run the 6-check safety gate | | Outcome | Success — PR #917 created, auto-merge enabled | | Files Changed | `tests/claude-code/shared/skills/worktree/worktree-create/SKILL.md` | ## When to Use Use this pattern when: - A safety/quality gate exists in one entry-
tools
# Orphan Config Detection ## Overview | Field | Value | |------------|-----------------------------------------------------------------| | Date | 2026-02-20 | | Issue | #777 | | PR | #824 | | Objective | Warn when a `config/models/*.yaml` file
tools
# Skill: model-config-naming-validation ## Overview | Field | Value | |------------|----------------------------------------------------| | Date | 2026-02-19 | | Issue | #682 | | PR | #769 | | Objective | CI check that filename matches model_id in YAML configs | | Outcome | Success — 28 tests