Problem
The three merge-gate hooks (`block-unreviewed-merge.sh`, `require-design-review-for-ui.sh`, `block-merge-on-red-ci.sh`) validate approval markers by comparing the marker SHA to `$(git rev-parse HEAD)` — i.e. the local HEAD. But `gh pr merge ` merges the PR's branch on GitHub, which is almost certainly NOT the local HEAD (local is usually `main` or a different feature branch).
Result: every `gh pr merge N` is rejected with:
```
BLOCKED: Code-reviewer approved commit but HEAD is now .
```
Workaround applied in every merge: `gh pr checkout N && gh pr merge N`. Tedious, error-prone, and creates extra local branches that have to be cleaned up. Hit on every merge during the 2026-04-14 merge train (6 PRs).
Proposed fix
In each merge-gate hook, read the PR's real HEAD from the command itself, not from local git state:
```bash
Extract PR number from the command (already done via _lib-extract-pr.sh)
PR_NUMBER=$(extract_pr_number "$COMMAND")
Read the PR's actual HEAD via gh — NOT via git rev-parse HEAD
PR_HEAD=$(gh pr view "$PR_NUMBER" --json headRefOid --jq .headRefOid 2>/dev/null)
Compare marker against PR_HEAD, not local HEAD
if [ "$(cat "$MARKER")" != "$PR_HEAD" ]; then
stale approval
fi
```
Needs the same change in all three hooks. Share the `PR_HEAD` resolution via `_lib-extract-pr.sh` (already exists after #47) for consistency.
Cross-repo caveat
`gh pr view` without `--repo` reads the current repo. For `gh api repos///pulls//merge` shape, the owner/repo is already extracted by `_lib-extract-pr.sh` — pass it to `gh pr view --repo <owner/repo> ` so cross-repo merges work (e.g. contributors from a fork).
Acceptance Criteria
Priority
P2 — cosmetic papercut, not a security hole. The hook is still correct in its intent (block stale approvals); it's just reading the wrong SHA source. Workaround is one extra command.
Related
Problem
The three merge-gate hooks (`block-unreviewed-merge.sh`, `require-design-review-for-ui.sh`, `block-merge-on-red-ci.sh`) validate approval markers by comparing the marker SHA to `$(git rev-parse HEAD)` — i.e. the local HEAD. But `gh pr merge ` merges the PR's branch on GitHub, which is almost certainly NOT the local HEAD (local is usually `main` or a different feature branch).
Result: every `gh pr merge N` is rejected with:
```
BLOCKED: Code-reviewer approved commit but HEAD is now .
```
Workaround applied in every merge: `gh pr checkout N && gh pr merge N`. Tedious, error-prone, and creates extra local branches that have to be cleaned up. Hit on every merge during the 2026-04-14 merge train (6 PRs).
Proposed fix
In each merge-gate hook, read the PR's real HEAD from the command itself, not from local git state:
```bash
Extract PR number from the command (already done via _lib-extract-pr.sh)
PR_NUMBER=$(extract_pr_number "$COMMAND")
Read the PR's actual HEAD via gh — NOT via git rev-parse HEAD
PR_HEAD=$(gh pr view "$PR_NUMBER" --json headRefOid --jq .headRefOid 2>/dev/null)
Compare marker against PR_HEAD, not local HEAD
if [ "$(cat "$MARKER")" != "$PR_HEAD" ]; then
stale approval
fi
```
Needs the same change in all three hooks. Share the `PR_HEAD` resolution via `_lib-extract-pr.sh` (already exists after #47) for consistency.
Cross-repo caveat
`gh pr view` without `--repo` reads the current repo. For `gh api repos///pulls//merge` shape, the owner/repo is already extracted by `_lib-extract-pr.sh` — pass it to `gh pr view --repo <owner/repo> ` so cross-repo merges work (e.g. contributors from a fork).
Acceptance Criteria
Priority
P2 — cosmetic papercut, not a security hole. The hook is still correct in its intent (block stale approvals); it's just reading the wrong SHA source. Workaround is one extra command.
Related