fix(process): #317 merge-consent gate auto-grants on satisfied path (no double-prompt)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m39s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m39s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The H6/H10 merge-consent hook's satisfied path did a bare `exit 0`, which does not auto-approve a PreToolUse tool call — it only declines to block, so control fell through to the normal permission system and the raw MCP prompt still fired. A ready-to-merge PR was therefore confirmed twice (conversationally + a redundant mechanical prompt). Emit permissionDecision "allow" (new `grant` decision) on the satisfied and docs-exempt paths so the derived state IS the consent; deny/ask unchanged (fail-closed); non-merge methods keep the exit-0 passthrough. Docs: CLAUDE.md, kickoff HARD CONSTRAINTS, docs/decisions.md (append-only, pure insert). Verified: 7 pipe tests (satisfied->allow, unticked->deny, stale->deny, red-CI->deny, no-verdict->ask, no-creds->ask, non-merge->passthrough). fixes #317 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,19 +13,32 @@
|
||||
# comment carrying a line `Review-verdict: <MERGEABLE|APPROVED|BLOCKED|NOT-MERGEABLE> @ <head-sha>`.
|
||||
#
|
||||
# Decision policy — a CONSENT gate, so it does NOT fail silently open:
|
||||
# - state derivable and satisfied -> grant (auto-approve: permissionDecision "allow",
|
||||
# so NO redundant permission prompt fires —
|
||||
# the derived state IS the consent, ersatztv#314)
|
||||
# - state derivable and NOT satisfied -> deny (actionable reason)
|
||||
# - state derivable and satisfied -> allow
|
||||
# - state NOT derivable (no creds, Gitea down,
|
||||
# no linked issue, no Done-when section) -> ask (surface to a human/session judgment)
|
||||
# Only a real merge is gated; every other pull_request_write method is allowed untouched.
|
||||
# Only a real merge is gated; every other pull_request_write method is passed through UNTOUCHED
|
||||
# (bare exit 0 → normal permissioning still applies), NOT auto-granted.
|
||||
#
|
||||
# WHY "grant" (not a bare exit 0) on the satisfied path (ersatztv#314 root cause): a PreToolUse hook
|
||||
# that exits 0 with no JSON does NOT auto-approve — it only declines to block, so control falls through
|
||||
# to the normal permission system and the raw MCP prompt still fires. The gate therefore only ever
|
||||
# ADDED a deny/ask net; it never REMOVED the baseline prompt on the happy path, so a satisfied merge
|
||||
# was confirmed twice (conversationally + a redundant mechanical prompt). Emitting permissionDecision
|
||||
# "allow" is what actually suppresses the prompt — "derive consent from state" made real.
|
||||
#
|
||||
# Gitea auth from env (never committed): ETV_GITEA_TOKEN (a token) OR ETV_GITEA_BASICAUTH (user:pass).
|
||||
# ETV_GITEA_URL overrides the base (default: the LAN instance; a LAN address, not a secret).
|
||||
set -euo pipefail
|
||||
input=$(cat)
|
||||
|
||||
decide() { # $1=allow|deny|ask $2=reason
|
||||
decide() { # $1=grant|allow|deny|ask $2=reason
|
||||
case "$1" in
|
||||
# grant = the gate is SATISFIED → auto-approve so no redundant permission prompt fires.
|
||||
grant) jq -n --arg r "$2" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:$r}}'; exit 0 ;;
|
||||
# allow = not our concern (non-merge method) → pass through untouched; normal permissioning applies.
|
||||
allow) exit 0 ;;
|
||||
deny) jq -n --arg r "$2" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'; exit 0 ;;
|
||||
ask) jq -n --arg r "$2" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:$r}}'; exit 0 ;;
|
||||
@@ -66,7 +79,7 @@ body=$(printf '%s' "$prjson" | jq -r '.body // ""' 2>/dev/null || true)
|
||||
# --- Docs-only exemption: if every changed file is docs/process, skip the gate. ---
|
||||
files=$(gq "repos/$owner/$repo/pulls/$pr/files?limit=100" | jq -r '.[].filename // empty' 2>/dev/null || true)
|
||||
if [ -n "$files" ] && ! printf '%s\n' "$files" | grep -qvE '^(docs/|\.claude/|\.husky/|\.gitea/|.*\.md$)'; then
|
||||
decide allow "" # all changed files are docs/process-only
|
||||
decide grant "H6/H10 merge gate: PR #$pr changes only docs/process files — gate not applicable, auto-granted."
|
||||
fi
|
||||
|
||||
# --- Linked issue: Gitea auto-close keywords in the PR body. ---
|
||||
@@ -151,7 +164,8 @@ if [ "$head_neg" = 1 ]; then
|
||||
decide deny "H10 merge gate: BLOCKED — a review verdict for the current head ($short) is negative (BLOCKED/NOT-MERGEABLE). Resolve the findings and post a fresh 'Review-verdict: MERGEABLE @ $short' before merging PR #$pr."
|
||||
fi
|
||||
if [ "$head_pos" = 1 ]; then
|
||||
decide allow "" # a positive verdict references the current head -> (c) satisfied
|
||||
# (a) CI green + (b) all Done-when ticked + (c) positive verdict @ current head -> SATISFIED. Auto-grant.
|
||||
decide grant "H6/H10 merge gate: satisfied — CI green, all Done-when boxes ticked, and a positive Review-verdict references the current head ($short). Auto-granted (no separate confirmation needed)."
|
||||
fi
|
||||
if [ "$stale" = 1 ]; then
|
||||
decide deny "H10 merge gate: BLOCKED — a review-verdict comment references an older commit, not the current head ($short). The latest commit(s) are unreviewed (ersatztv#242: re-review the fix commit, not just the initial diff). Re-review the head and post 'Review-verdict: MERGEABLE @ $short'."
|
||||
@@ -159,5 +173,5 @@ fi
|
||||
# Marker(s) exist but reference no sha at all -> ask (don't mislabel as a stale older-commit review).
|
||||
decide ask "H10 merge gate: a 'Review-verdict:' comment on PR #$pr references no commit sha. Post one referencing the current head ($short) — e.g. 'Review-verdict: MERGEABLE @ $short' — or confirm the review covered the latest commit and approve."
|
||||
|
||||
# All derivable and satisfied -> allow.
|
||||
decide allow ""
|
||||
# All derivable and satisfied -> auto-grant (defensive: the head_pos branch above already exits here).
|
||||
decide grant "H6/H10 merge gate: satisfied — auto-granted."
|
||||
|
||||
Reference in New Issue
Block a user