Skip to content

guard: claudemd cap checks the merge union (GAP4) - #710

Open
matt82198 wants to merge 3 commits into
mainfrom
guard/claudemd-headroom
Open

guard: claudemd cap checks the merge union (GAP4)#710
matt82198 wants to merge 3 commits into
mainfrom
guard/claudemd-headroom

Conversation

@matt82198

Copy link
Copy Markdown
Owner

Problem

The CLAUDE.md line cap only ever saw the branch. A branch could sit at 149/150 and pass while origin/main independently grew, so the merge landed at 151 and busted the cap on main with nothing red on the way in — three such cascades in one day.

Fix

tools/claudemd_lint.py --headroom is a new mode that lints the merge union instead of the working tree:

  • previews the merge with git merge-tree --write-tree, falling back to a per-file three-way git merge-file over the blobs on pre-2.38 git;
  • applies the same cap and the same per-file ALLOWED_OVERSIZE allowance to the union's line count (hoisted to a module constant so the two paths cannot drift);
  • fails a file whose union busts its cap even when the branch alone is under it.

Flags: --headroom [--base-ref origin/main] [--head-ref HEAD].
Exit contract: 0=clean, 1=a union busts its cap, 2=union unreadable (not a git repo, unresolvable ref, undecodable blob).

Wired into hooks/pre-push-policy.sh as check_claudemd_headroom (check 10). Exit 1 is fail-closed (push blocked); exit 2 and a missing tool fail open with claudemd_headroom_skipped_unreadable / _tool_missing logged — an unfetched base ref is an environment condition, not a policy violation, matching the existing fail-open-for-environment / fail-closed-for-policy split.

TDD evidence

Fixture: base = 100 body lines; the branch prepends 48 (149 counted lines — passes the cap outright) and main appends 2, so the union is 151 and must fail.

Behavioural RED/GREEN on that fixture:

--- OLD linter (working tree only) on the branch:
[OK] No issues found
OLD EXIT=0                      <-- the escape

--- NEW --headroom (merge union vs main):
1. [headroom-line-count] tools/CLAUDE.md: merge union with main is 151 lines,
   exceeds max 150 (branch alone: 149) -- trim before merging
NEW EXIT=1

Import-level RED (new tests against the pre-change module): ImportError: cannot import name 'check_headroom'.

Tests

  • tests/test_claudemd_lint.py: +10 cases (TestHeadroomMergeUnion) — branch-passes/union-busts, CLI exit 1, union clean, CLI exit 0, missing base ref → HeadroomError, non-git dir → HeadroomError, CLI exit 2, per-file oversize allowance parity, three-way fallback agrees with merge-tree, uncommitted working-tree edits excluded. Suite: 45 passed (was 35).
  • bash hooks/pre-push-policy.sh --test: 19/19 (new Test 19 asserts all three arms of the gate's exit contract — missing tool and exit 2 fail open, exit 1 fails closed).
  • Full Python suite (python -m unittest discover -s tests): green.
  • Repo gates clean: encoding_lint, watcher_linter, verify_test_coverage, metrics_gate, test_test_hygiene, test_subprocess_guard. claudemd_drift and dispatch_lint findings are byte-identical to the origin/main baseline (verified against a detached checkout) — no regression.

Dogfooded

tools/CLAUDE.md sits at exactly 150 on main, so the new entry was written into the existing line rather than appended. --headroom --base-ref origin/main on this branch is green, and the gate ran for real in this PR's own push.

🤖 Generated with Claude Code

The CLAUDE.md line cap only ever saw the branch. A branch could sit at
149/150 and pass while origin/main independently grew, so the merge landed
at 151 and busted the cap on main with nothing red on the way in -- three
such cascades in one day.

Add a `--headroom` mode to tools/claudemd_lint.py that lints the MERGE
UNION instead of the working tree: preview the merge with
`git merge-tree --write-tree` (falling back to a per-file three-way
`git merge-file` over the blobs on pre-2.38 git), then apply the same cap
and per-file oversize allowance to the union's line count. A file whose
union busts its cap fails even when the branch alone is under it.

Exit contract: 0=clean, 1=a union busts its cap, 2=union unreadable (not a
git repo, unresolvable ref, undecodable blob). Wired into
hooks/pre-push-policy.sh as check_claudemd_headroom, where exit 1 is
fail-closed (push blocked) and exit 2 is fail-open with an audit event --
an unfetched base ref is an environment condition, not a policy violation.

TDD: fixture repo where base=100 body lines, the branch prepends 48 (149
counted lines, passes the cap outright) and main appends 2, so the union is
151 and must fail. Behavioural RED/GREEN: the pre-change linter exits 0 on
that fixture, --headroom exits 1 naming the file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@matt82198 matt82198 added the merge-queue Queued for the merge-queue advancer daemon label Aug 3, 2026
matt82198 and others added 2 commits August 3, 2026 15:02
# Conflicts:
#	tests/test_claudemd_lint.py
#	tools/CLAUDE.md
#	tools/claudemd_lint.py
…he derivation)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-queue Queued for the merge-queue advancer daemon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant