Files
ersatztv/docs/decisions/records/release/merge-consent-autogrant.md
T
timothy fba5233caf
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Failing after 23s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m17s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m29s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m5s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 17m6s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
feat(610): split the decision corpus into one YAML-frontmatter file per record
168 records -> docs/decisions/records/<area>/<topic>.md (163 active, 23 dirs) and
docs/decisions/archive/<area>/<topic>.md (5 archived). The filename IS the key,
so one-active-record-per-key becomes a filesystem property rather than a
validator check, and supersession becomes a `git mv`.

WHY: the monolith was a concurrency problem before an aesthetic one. A
3,900-line append target made parallel sessions collide -- PR #605 and PR #614
both hit append-vs-append conflicts during routine rebases, and hand-resolving
those inside the corpus is exactly the operation the rationale-rewrite guard
exists to police.

HOW IT IS VERIFIED: a ~170-file diff cannot be meaningfully read, so correctness
does not rest on reading it. The parser was taught BOTH formats first, so the
body-diff guard parses the old form at the merge-base and the new form at head --
the migration validates itself, no bypass. The proof is a field-level equivalence
harness: 168 records before and after, zero lost, zero gained, zero field
mismatches, zero rationale bodies differing. Reviewers should scrutinise the
harness; it is the actual evidence.

What measuring caught that reading would not have:

- ~500 lines sit OUTSIDE any record -- decisions.md's lifecycle schema and each
  topic file's preamble, mostly the only copy. Source files are kept and
  stripped, never deleted. They also cannot be filed per-area: topic files hold
  several areas and 4 of 23 areas span several files.
- Archive discovery was a non-recursive glob; after the split it found ZERO
  archived records, surfacing as four bogus "supersedes points to unknown key"
  errors rather than an obvious failure.
- ~32 live docs point into the corpus BY DATE, which the split dangles. Each
  stripped file now ends with a generated "Records formerly in this file" index,
  which also rescues the identical breadcrumbs in old issue comments.
- decisions.md's "In this file:" list was 97 same-file anchor bullets that the
  split makes WRONG, not merely stale. Dropped; the generated index replaces
  them with links that resolve.

The equivalence harness now runs against a checked-in FIXTURE, not the live
corpus. The earlier version migrated the real tree, which made it a one-shot:
the moment the migration landed there was nothing left to move and the tests
failed for reasons unrelated to the code. A fixture keeps them testing the
SCRIPT rather than the repo's current state.

Keys preserved verbatim, warts included: `sched` (12) and `scheduling` (1) remain
two directories for one concept. Renaming a key is not a move -- it changes
identity, breaks the equivalence proof, and invalidates MemPalace's per-key
drawers. Taxonomy normalisation is separate work.

refs #610
2026-07-25 19:45:09 +02:00

3.3 KiB

key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
key title status since supersedes superseded-by rule signals mechanics
release.merge-consent-autogrant 2026-07-12 — Merge-consent gate auto-grants when satisfied (no redundant prompt); state IS the consent (#314) active 2026-07-12 none none When Done-when boxes are ticked, CI is green, and a fresh positive Review-verdict references head, the merge-consent hook emits `permissionDecision: allow` to actually suppress the redundant mechanical prompt — the derived state IS the consent, no separate conversational confirmation on that path. auto-grant, permissionDecision allow, merge consent · paths: `.claude/settings.json` · issues: #314, #303, #317 `pretooluse-merge-consent.sh`; CLAUDE.md → Task Completion Protocol

Completes the #303 H6/H10 intent — derive merge-consent from state — which the original hook only half-delivered. The rule the user set: merge permission is auto-granted for the session when the linked issue's ## Done-when boxes are all ticked, a fresh positive Review-verdict references the current head, and CI is green — no separate confirmation, conversational or mechanical.

Root cause of the bug this fixes: 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 permission prompt still fires (the merge tool isn't allow-listed). So the gate only ever added a deny/ask net; it never removed the baseline prompt on the happy path. Net effect for the operator: a ready-to-merge PR was 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: 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. 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 warranted only when the gate asks (state not derivable). This supersedes the "always confirm merge consent in-conversation per session" phrasing in the kickoff HARD CONSTRAINTS (updated in the same PR).