Skip to content

chore(#115): warn-stale-review-markers.sh PostToolUse hook#120

Merged
atlas-apex merged 1 commit into
devfrom
chore/GH-115-warn-stale-review-markers
Apr 24, 2026
Merged

chore(#115): warn-stale-review-markers.sh PostToolUse hook#120
atlas-apex merged 1 commit into
devfrom
chore/GH-115-warn-stale-review-markers

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

Summary

Adds a new PostToolUse hook warn-stale-review-markers.sh that fires after git push and surfaces review markers invalidated by new commits — immediately, instead of at gh pr merge time.

The gap it closes: when an author pushes new commits to a PR that already has Rex / CEO / design approvals on record, those markers are now stale (their recorded SHA ≠ PR HEAD). The merge gate catches this, but only at merge attempt — which can be hours or days after the push. Result: confusing "why is my merge blocked, Rex already approved?" moments. This hook makes the invalidation visible the moment it happens.

  • New hook: .claude/hooks/warn-stale-review-markers.sh (~135 LOC, PostToolUse, non-blocking)
  • Tests: .claude/hooks/tests/test_warn_stale_review_markers.sh — 8 cases, all pass
  • Config integration: reads .review_markers.on_stale (warn default, delete opt-in) via the shared config lib landed in [Chore] Make ticket-prefix whitelist + schema project-configurable (not hardcoded) #109
  • Defaults: extended .claude/project-config.defaults.json with a review_markers subtree
  • Wired into .claude/settings.json — PostToolUse matcher on Bash commands matching git push
  • Audit entry: new row in Section 3 of docs/rule-audit.md

How it works

  1. Fires after a successful git push. Non-blocking by design — PostToolUse exit 2 would push noise into the conversation; this hook is purely informational.
  2. Resolves the PR number for the current branch via gh pr view --json number. Silent exit 0 if no PR.
  3. Resolves the PR HEAD via gh pr view --json headRefOid — same source of truth the merge-gate hooks use post-fix: merge gate hooks don't catch gh api .../merge bypass #47 / [Bug] Merge-gate hooks read local HEAD instead of PR HEAD — forces manual 'gh pr checkout' before every merge #55. Falls back to local HEAD with a visible warning if gh is offline.
  4. Scans $(git rev-parse --show-toplevel)/.claude/session/reviews/<pr>-*.approved for files whose recorded SHA doesn't match HEAD.
  5. warn mode (default): prints one stderr line per stale marker, naming the file and the old/new SHAs, suggesting re-invocation.
  6. delete mode (opt-in via review_markers.on_stale: "delete"): removes stale markers so the merge gate's "marker missing" message is crisper than "marker stale".
  7. Silent on push failure (detected via stderr patterns from the git push output and the natural property that a failed push means the remote HEAD didn't advance — markers stay fresh).

Testing

Ran the committed test fixture — 8 cases, all pass:

$ bash .claude/hooks/tests/test_warn_stale_review_markers.sh
PASS [no-pr-silent]
PASS [no-markers-silent]
PASS [fresh-markers-silent]
PASS [stale-rex-warn]
PASS [stale-ceo-warn]
PASS [stale-design-warn]
PASS [stale-rex-delete]
PASS [push-failed-silent]
PASS: 8   FAIL: 0

Small tangential fix in the test fixture: the sandbox setup now copies the shared config reader (_lib-read-config.sh) and the shipped defaults (project-config.defaults.json) into the isolated repo, so the sandboxed hook resolves config the same way it would in a real fork. Previously the sandbox didn't include the shared lib, and the delete-mode test would silently fall back to warn because config_get_or returned the default. Not a production issue (real forks always have both files after #109), but a correctness improvement to the test harness.

Scope — what this does NOT do

  • Does not block the push. This hook is advisory.
  • Does not change the merge-gate hook (block-unreviewed-merge.sh) — that still catches stale markers at merge time as a final backstop.
  • Does not touch /approve-merge, /approve-design, or the Rex agent.
  • --no-verify pushes bypass this hook (same as any PostToolUse). Acceptable — bypass discipline is covered by the existing rule ("never skip hooks").

Glossary

Term Definition
Review marker A file under .claude/session/reviews/<pr>-<reviewer>.approved containing the 40-char SHA of the approved commit. Written by Rex / /approve-merge / /approve-design.
Stale marker A marker whose SHA no longer matches the PR's HEAD — typically because new commits were pushed after approval.
PostToolUse A Claude Code hook that runs AFTER a tool call succeeds. Cannot block; used for warnings, cleanup, and auditing.
warn / delete The two modes this hook supports. Default warn prints a stderr line per stale marker; opt-in delete removes the marker so the merge gate reports "missing" rather than "stale".

Closes #115

Loading
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