diff --git a/.claude/hooks/pretooluse-merge-consent.sh b/.claude/hooks/pretooluse-merge-consent.sh index c21580902..c5b1e5592 100755 --- a/.claude/hooks/pretooluse-merge-consent.sh +++ b/.claude/hooks/pretooluse-merge-consent.sh @@ -79,7 +79,13 @@ 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 grant "H6/H10 merge gate: PR #$pr changes only docs/process files — gate not applicable, auto-granted." + # Docs/process-only PR: the Done-when + review-verdict gate doesn't apply — but this exemption is a + # file-TYPE bypass, NOT the a+b+c "provably reviewed & ready" proof, so it does NOT auto-grant. It + # passes through to normal permissioning (one prompt). This deliberately keeps a human in the loop for + # process-control files (.claude/ / .gitea/ / .husky/ — the gate, CI, and git hooks themselves): a PR + # that weakens the gate must not silently self-merge (ersatztv#317 review nit). Only the satisfied + # merge path below auto-grants. + decide allow "" # passthrough (exit 0 → normal prompt), NOT grant fi # --- Linked issue: Gitea auto-close keywords in the PR body. --- diff --git a/docs/decisions.md b/docs/decisions.md index dd580886e..f248b6a20 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1537,14 +1537,18 @@ still fires (the merge tool isn't allow-listed). So the gate only ever *added* a confirmed twice — once conversationally (the per-session merge-consent norm) and again by a redundant mechanical prompt the gate was supposed to have subsumed. -**Fix:** the satisfied and docs-exempt paths now emit +**Fix:** ONLY the genuinely-satisfied merge path (a+b+c all true) now emits `{"hookSpecificOutput":{"permissionDecision":"allow", ...}}` (a new `grant` decision), which actually suppresses the prompt. Deny (unticked/red/negative/stale) and ask (non-derivable: no creds, Gitea down, no linked issue, no `## Done-when`, no verdict) are unchanged — the gate still fails closed, not -open. Non-merge `pull_request_write` methods keep the bare `exit 0` **passthrough** (normal -permissioning still applies) — auto-grant is scoped to the merge decision only, not broadened to -PR-create/edit. Verified by 7 pipe tests (satisfied→allow, unticked→deny, stale→deny, red-CI→deny, -no-verdict→ask, no-creds→ask, non-merge→passthrough). +open. Two paths deliberately do **not** auto-grant and keep the bare `exit 0` **passthrough** (normal +permissioning → one prompt): non-merge `pull_request_write` methods (auto-grant is scoped to +method=merge only), and the **docs/process-only exemption**. The exemption is a file-TYPE bypass, not +the a+b+c "provably reviewed & ready" proof, so it must not *silently* self-merge — critically, its set +includes `.claude/`/`.gitea/`/`.husky/` (the gate, CI workflows, and git hooks themselves), so a PR +that weakens the gate still gets a human prompt (ersatztv#317 review nit). Verified by 8 pipe tests +(satisfied→allow, docs-only→passthrough, unticked→deny, stale→deny, red-CI→deny, no-verdict→ask, +no-creds→ask, non-merge→passthrough). **Process consequence:** the state-derived gate *is* the consent on the satisfied path — do **not** also ask conversationally to merge a PR whose gate auto-grants. A separate human confirmation is still