Skip git-ignored files in the length gate, and vendored skill bundles

Three exemptions the first cut missed, all of them files no commit could
ever fix:

- Git-ignored files (todo's exported BACKLOG.md, logs, scratch dumps).
  These are rewritten wholesale by whatever produces them, so splitting one
  is meaningless, and pre-commit never sees them anyway. Applied to
  explicitly-named paths as well as --all, so the two modes cannot disagree
  about the same file.
- Vendored agent-skill bundles (.agents/skills, .claude/skills). A SKILL.md
  is a fixed-name file with frontmatter; splitting one breaks the format.
  Matched by directory name wherever it appears, since these get dropped
  into any repo.
- .utils, where CI checks out this repo. Without it a repo's lint job fails
  on utils' own long files, which that repo cannot fix.

Baseline accordingly drops from 657 to 597. Re-verified all eight fixtures:
over-cap fails, under-cap passes, generated/vendored/wordlist/git-ignored
exempt, prose still capped, and the gate still passes on its own source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7yZz45ezmzCB89Cv5gAtZ
This commit is contained in:
Krzysztof Rudnicki 2026-08-14 15:58:46 +02:00
parent c54211f112
commit ca81e091a1
3 changed files with 749 additions and 1710 deletions

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ from __future__ import annotations
import argparse
import os
import subprocess
import sys
from pathlib import Path
@ -61,14 +62,45 @@ def exempt_reason(path: Path, lines: int) -> str | None:
return None
def git_ignored(paths: list[Path], root: Path) -> set[Path]:
"""The subset of `paths` that git ignores, empty if root is not a repo.
Git-ignored files are build output or generated data the repo does not
own -- an app's own export (todo's BACKLOG.md), a log, a scratch dump.
They are rewritten wholesale by whatever produces them, so splitting one
is meaningless: the next run restores the original. Pre-commit never sees
them either (they cannot be staged), so skipping them makes --all agree
with the hook instead of failing on files no commit could ever fix.
"""
if not paths:
return set()
try:
result = subprocess.run(
["git", "-C", str(root), "check-ignore", "--stdin"],
input="\n".join(str(p) for p in paths),
capture_output=True,
text=True,
check=False,
)
except OSError: # git not installed
return set()
# 0 = some ignored, 1 = none ignored, 128 = not a repo.
if result.returncode not in (0, 1):
return set()
return {Path(line) for line in result.stdout.splitlines() if line}
def iter_all(root: Path):
"""Every file under root, skipping excluded directories."""
"""Every file under root, skipping excluded and git-ignored files."""
found: list[Path] = []
for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS]
for name in filenames:
candidate = Path(dirpath) / name
if not candidate.is_symlink():
yield candidate
found.append(candidate)
ignored = git_ignored(found, root)
return [path for path in found if path not in ignored]
def main() -> int:
@ -89,7 +121,13 @@ def main() -> int:
if args.all:
targets = list(iter_all(Path.cwd()))
elif args.paths:
targets = args.paths
# Explicitly-named paths get the same git-ignore skip as --all, so
# naming a file directly cannot report a violation the hook never
# would. Without this the two modes disagree on exactly the files
# no commit could ever fix (todo's exported BACKLOG.md, logs).
existing = [p for p in args.paths if p.is_file()]
ignored = git_ignored(existing, Path.cwd())
targets = [p for p in existing if p not in ignored]
else:
parser.error("give file paths or --all")
@ -118,9 +156,7 @@ def main() -> int:
for path, lines in sorted(violations, key=lambda item: -item[1]):
over = lines - MAX_LINES
print(f" {path}: {lines} lines (over by {over})", file=sys.stderr)
print(
"\nSplit them, or see refactor_claude_todo.md in this repo.", file=sys.stderr
)
print("\nSplit them, or see refactor_claude_todo.md in this repo.", file=sys.stderr)
return 1

View File

@ -121,6 +121,10 @@ EXCLUDED_DIRS = frozenset(
".ci-mirror-venv",
".hippo",
".build-deps",
# Where CI checks out the shared gate itself (kuhyx/utils). Without
# this, a repo's lint job fails on utils' own long files -- a gate
# that fails for reasons the repo being gated cannot fix.
".utils",
}
)
@ -142,8 +146,18 @@ VENDORED_SUBPATHS = {
"plugins",
"ide",
),
# Agent skill bundles vendored from Anthropic (todo: commit f91311f).
# A SKILL.md is a fixed-name file with frontmatter; detail belongs in a
# sibling references/ dir, so splitting one into SKILL.part2.md would
# break the format rather than improve readability.
"todo": (".agents/skills",),
}
#: Directory names that mark a vendored agent-skill tree wherever they appear.
#: Keyed by name rather than repo because these bundles get dropped into any
#: repo that wants them, not just the one they were first seen in.
VENDORED_ANYWHERE = ("/.agents/skills/", "/.claude/skills/")
#: Whole repos under ~ that are third-party clones, not kuhy's work.
#: Verified 2026-08-14 by `git remote get-url origin` + commit authors.
THIRD_PARTY_REPOS = frozenset(
@ -195,7 +209,13 @@ def is_vendored(path: Path) -> bool:
parts = path.parts
if any(part in EXCLUDED_DIRS for part in parts):
return True
# Pre-commit passes paths relative to the repo root ('.agents/skills/x'),
# while --all walks absolute ones ('/home/kuhy/todo/.agents/skills/x').
# Compare against a leading-slash form so one rule covers both modes.
posix = path.as_posix()
anchored = posix if posix.startswith("/") else f"/{posix}"
if any(marker in anchored for marker in VENDORED_ANYWHERE):
return True
for repo, subs in VENDORED_SUBPATHS.items():
for sub in subs:
if f"/{repo}/{sub}/" in posix or posix.startswith(f"{repo}/{sub}/"):