fix(process): #317 merge-consent gate auto-grants on satisfied path (no double-prompt) #318

Merged
timothy merged 2 commits from fix/merge-gate-autogrant into main 2026-07-12 21:05:28 +02:00
Owner

Closes #317.

Problem (user-reported)

Merging a PR whose ## Done-when boxes are ticked and whose review is satisfied still forced two confirmations: a conversational ask and a redundant mechanical MCP permission prompt.

Root cause

pretooluse-merge-consent.sh's satisfied path did a bare exit 0. 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__gitea__pull_request_write prompt still fires. The gate therefore only ever added a deny/ask net; it never removed the baseline prompt on the happy path.

Fix

Only the genuinely-satisfied merge path (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ current head) now emits {"hookSpecificOutput":{"permissionDecision":"allow", ...}} (new grant decision) — the derived state IS the consent. Deny (unticked/red/negative/stale) and ask (non-derivable) are unchanged: the gate still fails closed.

Two paths deliberately do not auto-grant and keep the bare exit 0 passthrough (one normal prompt): non-merge pull_request_write methods, and the docs/process-only exemption. The exemption's set includes .claude//.gitea//.husky/ — the gate, CI, and hooks themselves — so a PR that weakens the gate must still get a human sign-off, not silently self-merge (addresses the first-pass adversarial-review nit).

Verification

8 pipe tests via a fake curl on PATH: satisfied→allow, docs-only→passthrough, unticked→deny, stale-sha→deny, red-CI→deny, no-verdict→ask, no-creds→ask, non-merge→passthrough. bash -n + shellcheck clean.

Docs (same PR)

  • CLAUDE.md Task Completion Protocol — auto-grant behaviour + "don't also ask conversationally on the satisfied path".
  • docs/handoffs/chicorytv-issue-queue.md HARD CONSTRAINTS — merge consent is derived; ask only when the gate asks.
  • docs/decisions.md — new #314/#317-lineage entry.

Docs/process-only PR (.claude/, docs/, CLAUDE.md) → this PR itself hits the docs-exemption passthrough, so merging it still prompts once — an intentional human sign-off on a change to the merge gate.

Closes #317. ## Problem (user-reported) Merging a PR whose `## Done-when` boxes are ticked and whose review is satisfied still forced **two** confirmations: a conversational ask **and** a redundant mechanical MCP permission prompt. ## Root cause `pretooluse-merge-consent.sh`'s satisfied path did a bare `exit 0`. 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__gitea__pull_request_write` prompt still fires. The gate therefore only ever *added* a deny/ask net; it never *removed* the baseline prompt on the happy path. ## Fix **Only the genuinely-satisfied merge path (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ current head)** now emits `{"hookSpecificOutput":{"permissionDecision":"allow", ...}}` (new `grant` decision) — the derived state IS the consent. Deny (unticked/red/negative/stale) and ask (non-derivable) are unchanged: the gate still fails closed. Two paths deliberately do **not** auto-grant and keep the bare `exit 0` passthrough (one normal prompt): non-merge `pull_request_write` methods, and the **docs/process-only exemption**. The exemption's set includes `.claude/`/`.gitea/`/`.husky/` — the gate, CI, and hooks themselves — so a PR that weakens the gate must still get a human sign-off, not silently self-merge (addresses the first-pass adversarial-review nit). ## Verification 8 pipe tests via a fake `curl` on PATH: satisfied→`allow`, docs-only→passthrough, unticked→`deny`, stale-sha→`deny`, red-CI→`deny`, no-verdict→`ask`, no-creds→`ask`, non-merge→passthrough. `bash -n` + shellcheck clean. ## Docs (same PR) - `CLAUDE.md` Task Completion Protocol — auto-grant behaviour + "don't also ask conversationally on the satisfied path". - `docs/handoffs/chicorytv-issue-queue.md` HARD CONSTRAINTS — merge consent is derived; ask only when the gate asks. - `docs/decisions.md` — new #314/#317-lineage entry. Docs/process-only PR (`.claude/`, `docs/`, `CLAUDE.md`) → this PR itself hits the docs-exemption **passthrough**, so merging it still prompts once — an intentional human sign-off on a change to the merge gate.
timothy added the bugci-cd labels 2026-07-12 20:53:05 +02:00
timothy added 1 commit 2026-07-12 20:53:05 +02:00
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
8565f731cd
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>
timothy added 1 commit 2026-07-12 20:58:29 +02:00
fix(process): #317 docs-only exemption stays passthrough, not silent auto-grant [decisions-edit]
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m22s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m33s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m36s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m33s
90c8348efe
Adversarial-review nit on the first commit: auto-granting the docs-only exemption
silently self-merges process-control PRs (.claude/.gitea/.husky — including the
gate hook itself) with no prompt and no review, bypassing human-in-the-loop for
exactly the files that control the gate. Restrict auto-grant to the genuinely-
satisfied (a+b+c) merge path; the docs/process exemption reverts to bare exit-0
passthrough (one normal prompt). Corrects this PR's own decisions.md entry
accordingly ([decisions-edit]: a not-yet-merged draft entry, not history).

8 pipe tests green (adds docs-only -> passthrough).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Adversarial review (2 independent cold passes)

Pass 1 — initial diff (8565f731): MERGEABLE-WITH-NITS. No false-open; all deny/ask paths byte-identical to main; non-merge methods still bare exit 0 passthrough; emitted JSON valid. Nit (acted on): the docs-only exemption was changed to grant too, which would silently auto-merge process-control PRs (.claude//.gitea//.husky/ — including the gate hook itself) with no prompt and no review.

Fix (90c8348e): reverted the docs-exemption to a bare exit 0 passthrough (one human prompt). Auto-grant is now scoped to ONLY the genuinely-satisfied (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ head) path.

Pass 2 — re-review of the fix commit (90c8348e): MERGEABLE, no nits. Confirmed the docs-exemption revert is the only behavioral change vs 8565f731, that it is a strict reduction of the auto-grant surface (monotonic tightening → cannot introduce a new false-open), that the docs-exemption now genuinely prompts a human (incl. PRs touching the gate itself), and that the satisfied a+b+c auto-grant path is unchanged and sound.

Verification: 8 pipe tests (satisfied→allow, docs-only→passthrough, unticked→deny, stale→deny, red-CI→deny, no-verdict→ask, no-creds→ask, non-merge→passthrough); bash -n + shellcheck clean.

Review-verdict: MERGEABLE @ 90c8348e

## Adversarial review (2 independent cold passes) **Pass 1 — initial diff (`8565f731`)**: MERGEABLE-WITH-NITS. No false-open; all deny/ask paths byte-identical to main; non-merge methods still bare `exit 0` passthrough; emitted JSON valid. **Nit (acted on):** the docs-only exemption was changed to `grant` too, which would *silently* auto-merge process-control PRs (`.claude/`/`.gitea/`/`.husky/` — including the gate hook itself) with no prompt and no review. **Fix (`90c8348e`)**: reverted the docs-exemption to a bare `exit 0` passthrough (one human prompt). Auto-grant is now scoped to ONLY the genuinely-satisfied (a=CI green, b=all Done-when ticked, c=positive Review-verdict @ head) path. **Pass 2 — re-review of the fix commit (`90c8348e`)**: MERGEABLE, no nits. Confirmed the docs-exemption revert is the *only* behavioral change vs `8565f731`, that it is a strict *reduction* of the auto-grant surface (monotonic tightening → cannot introduce a new false-open), that the docs-exemption now genuinely prompts a human (incl. PRs touching the gate itself), and that the satisfied a+b+c auto-grant path is unchanged and sound. Verification: 8 pipe tests (satisfied→allow, docs-only→passthrough, unticked→deny, stale→deny, red-CI→deny, no-verdict→ask, no-creds→ask, non-merge→passthrough); `bash -n` + shellcheck clean. Review-verdict: MERGEABLE @ 90c8348e
timothy merged commit 90c8348efe into main 2026-07-12 21:05:28 +02:00
timothy deleted branch fix/merge-gate-autogrant 2026-07-12 21:05:28 +02:00
Sign in to join this conversation.