Skip to content

fix(#52): reject closed issue refs in PR and commit hooks#53

Merged
atlas-apex merged 1 commit into
mainfrom
fix/#52-reject-closed-issue-refs
Apr 14, 2026
Merged

fix(#52): reject closed issue refs in PR and commit hooks#53
atlas-apex merged 1 commit into
mainfrom
fix/#52-reject-closed-issue-refs

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

Summary

  • validate-pr-create.sh: in addition to rejecting PRs that reference a non-existent issue, now also reject PRs that reference a closed issue. Every PR needs its own OPEN ticket.
  • verify-commit-refs.sh: same fetch on the commit-level hook, but CLOSED is warn (stderr, exit 0), not block. A commit may legitimately reference the issue it just closed (reverts, follow-ups); the PR-level check is the right place to enforce the "open ticket" rule.

Why

PR me2resh/curios-dog#218 was created with title fix(#210): ... but #210 was already closed. The old hook passed because gh issue view #210 still resolves successfully for closed issues — it just didn't check state. The CEO caught it by eye, but the hook is supposed to be the backstop for exactly this failure mode, so it's closing an observability gap in the governance layer.

Full incident + reproduction context lives on the upstream issue:
#52

Changes

.claude/hooks/validate-pr-create.sh — one call becomes one call that captures both number and state:

-if ! gh issue view "$TICKET_NUM" --repo "$TRACKER_REPO" --json state >/dev/null 2>&1; then
+ISSUE_JSON=$(gh issue view "$TICKET_NUM" --repo "$TRACKER_REPO" --json number,state 2>/dev/null)
+if [ -z "$ISSUE_JSON" ]; then
   # ... existing "does not exist" block
 fi
+ISSUE_STATE=$(echo "$ISSUE_JSON" | jq -r '.state // empty')
+if [ "$ISSUE_STATE" = "CLOSED" ]; then
+  # new "issue is closed" block with the three-cause explanation
+fi

The new block message lists the three common root causes (follow-up, premature auto-close, typo) and tells the author what to do — create a new open ticket, reopen, or fix the number.

.claude/hooks/verify-commit-refs.sh — tracks missing and closed separately. Missing still blocks, closed warns:

-if ! gh issue view "$NUM" --repo "$TRACKER_REPO" --json state >/dev/null 2>&1; then
-  MISSING="${MISSING}${REF} "
+ISSUE_JSON=$(gh issue view "$NUM" --repo "$TRACKER_REPO" --json number,state 2>/dev/null)
+if [ -z "$ISSUE_JSON" ]; then
+  MISSING="${MISSING}${REF} "
+  continue
 fi
+ISSUE_STATE=$(echo "$ISSUE_JSON" | jq -r '.state // empty')
+if [ "$ISSUE_STATE" = "CLOSED" ]; then
+  CLOSED="${CLOSED}${REF} "
+fi

Testing

Self-tested on the ops fork:

  • ✅ Committing with a fake #99999 → blocks (existing behavior preserved)
  • ✅ Committing with a closed real issue → warns on stderr, commit proceeds
  • ✅ Creating a PR whose title references a closed issue → blocks
  • ✅ Creating a PR whose title references an open issue → passes
  • bash -n syntax check passes on both scripts

Note on testing: the commit message of this very PR could not use Closes #52 because the commit-level hook (on this ops-repo fork) detects me2resh/ops as the tracker via git remote get-url origin, but issue #52 lives on me2resh/apexstack (upstream). The hook treats that as "issue does not exist in tracker" and blocks. Workaround here: cite the upstream issue by URL in the commit body. A follow-up ticket is worth filing on upstream to let contributors from forks reference upstream issues via full URL syntax — but that's out of scope for this fix.


Glossary

Term Definition
Tracker repo The GitHub repo that hosts the project's issues. For apexstack (and each managed project in the ApexStack portfolio) this is the project's own repo. Hooks infer it from .claude/project-config.json or from git remote get-url origin.
Cross-fork PR This PR's source branch lives on me2resh/ops (a fork of apexstack), but the PR targets me2resh/apexstack:main. Standard OSS contribution pattern.
--json number,state Fetching both fields in one gh issue view call so the hook can distinguish "does not exist" (empty response) from "exists but CLOSED" (non-empty, state=CLOSED). Cheaper than two separate HTTP calls.
Backstop hook A hook that validates work at the moment it becomes a durable artifact (PR title, commit message), as opposed to catching the root cause in prose. The primary fix for "referenced closed issue" is self-discipline — this hook is the backstop when discipline fails.
Closes #N / Refs #N / Fixes #N / Resolves #N GitHub-recognized issue linking keywords in commit messages. GitHub auto-closes the referenced issue on merge for Closes/Fixes/Resolves.
WARN vs BLOCK In ApexStack's hook convention, exit 2 blocks the tool use (commit refused); anything written to stderr with exit 0 is a warning that the user sees but the action proceeds. Chosen per-hook based on how often a false positive would be disruptive.

- validate-pr-create.sh: also capture issue state. Block PR when the
  referenced issue is CLOSED (in addition to the existing "does not
  exist" block). Blocking message explains the three common causes
  (follow-up, auto-closed premature, typo) and what to do instead.
- verify-commit-refs.sh: same fetch, but CLOSED is WARN (stderr,
  exit 0) rather than BLOCK. A commit may legitimately reference the
  closed issue it just finished — the PR-level hook is the right
  place to enforce "every PR needs its own OPEN ticket".

Prevents incidents like the one described in the upstream issue:
#52

Upstream issue: #52

@atlas-apex atlas-apex left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review: PR #53

Commit: 2960b4dc9d803dbe62350170df2d627b240c04ec

Summary

Tightens the ticket-vocabulary backstop hooks so that PR titles referencing a CLOSED issue are blocked, while commit-level references to CLOSED issues are warned (not blocked). Closes the observability gap revealed by me2resh/curios-dog#218.

Checklist Results

  • Architecture & Design: Pass
  • Code Quality: Pass
  • Testing: Pass (manual smoke tests documented in PR body; no harness exists for hooks)
  • Security: Pass (no new shellouts beyond gh/jq, both already used)
  • Performance: Pass (still one gh issue view per ref, same as before)
  • PR Description & Glossary: Pass — Glossary is thorough (tracker repo, cross-fork PR, --json number,state, backstop hook, GitHub linking keywords, WARN vs BLOCK)
  • Technical Decisions (AgDR):N/A — incremental tightening of an existing rule, no new pattern/library/architecture choice

Verification

  • bash -n clean on both scripts
  • Live gh issue view --json number,state against me2resh/apexstack: missing issue → empty [] (correctly triggers BLOCK as missing); CLOSED issue (#49) → {"number":49,"state":"CLOSED"} (correctly triggers CLOSED branch)
  • The OPEN path is unchanged — the new ISSUE_STATE check only fires on = "CLOSED", so any other state (OPEN, or missing-state edge case) falls through unaffected
  • WARN-vs-BLOCK split between the two hooks matches the stated rationale and is well-commented inline

Findings

None blocking.

Suggestions (non-blocking, follow-ups)

  1. Cross-repo issue refs from forks — file as a separate ticket. The commit-level hook on a fork can't reference upstream issues by #N. Workaround (full URL in body) is fine; longer-term, consider letting .claude/project-config.json declare an upstream_tracker_repo that the hook also checks.
  2. Hook test harness — file as a separate ticket. Two hooks now have non-trivial branching logic; a tiny bats (or pure-bash) harness with mocked gh would let us regression-test the missing/CLOSED/OPEN matrix.
  3. Note: gh issue view <N> resolves PRs too (issues and PRs share the numbering namespace). Pre-existing behavior, not introduced by this PR — flagging only because it means a PR-number typo in a title can theoretically pass the existence check. Out of scope here.

Verdict

APPROVED (posted as comment — cannot self-approve as PR author)


Reviewed by Rex (Code Reviewer Agent)
Reviewed commit: 2960b4dc9d803dbe62350170df2d627b240c04ec

@atlas-apex atlas-apex merged commit 5f067b5 into main Apr 14, 2026
2 checks passed
@atlas-apex atlas-apex deleted the fix/#52-reject-closed-issue-refs branch April 14, 2026 07:17
osama-abu-baker pushed a commit to osama-abu-baker/apexyard that referenced this pull request Jun 3, 2026
- validate-pr-create.sh: also capture issue state. Block PR when the
  referenced issue is CLOSED (in addition to the existing "does not
  exist" block). Blocking message explains the three common causes
  (follow-up, auto-closed premature, typo) and what to do instead.
- verify-commit-refs.sh: same fetch, but CLOSED is WARN (stderr,
  exit 0) rather than BLOCK. A commit may legitimately reference the
  closed issue it just finished — the PR-level hook is the right
  place to enforce "every PR needs its own OPEN ticket".

Prevents incidents like the one described in the upstream issue:
me2resh#52

Upstream issue: me2resh#52

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
mosta7il pushed a commit to mosta7il/apexyard that referenced this pull request Jun 8, 2026
- validate-pr-create.sh: also capture issue state. Block PR when the
  referenced issue is CLOSED (in addition to the existing "does not
  exist" block). Blocking message explains the three common causes
  (follow-up, auto-closed premature, typo) and what to do instead.
- verify-commit-refs.sh: same fetch, but CLOSED is WARN (stderr,
  exit 0) rather than BLOCK. A commit may legitimately reference the
  closed issue it just finished — the PR-level hook is the right
  place to enforce "every PR needs its own OPEN ticket".

Prevents incidents like the one described in the upstream issue:
me2resh#52

Upstream issue: me2resh#52

Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants