Exempt frozen session artifacts from the 250-line cap

An implementation plan under docs/superpowers/plans/ and an append-only log
under docs/superpowers/sessions/ are frozen records of one past session. They
are read whole or not at all, so splitting one rewrites a historical record
for no reading benefit -- the same reason generated files are exempt.

Scoped deliberately: specs/ and contracts/ stay capped, because those are
living documents that get re-read and edited.

is_session_artifact() is kept separate from is_vendored() so the gate reports
an accurate reason -- these are kuhy's own files, not third-party code.

Relaxes the gate only, so no already-wired repo can newly fail. Verified:
~/todo still exits 0 on --all; testsAndMisc drops 198 -> 197 violations
(plans/ exempt, specs/ still caught) in both absolute and relative modes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 15:06:42 +02:00
parent 682eaf6f8c
commit f8dded33c7
2 changed files with 58 additions and 11 deletions

View File

@ -11,19 +11,20 @@ from __future__ import annotations
import argparse
import os
from pathlib import Path
import subprocess
import sys
from pathlib import Path
if __package__ in (None, ""): # invoked as a plain script
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from file_length.config import ( # noqa: E402
from file_length.config import (
CAPPED_EXTENSIONS,
EXCLUDED_DIRS,
MAX_LINES,
is_data_text,
is_generated,
is_session_artifact,
is_vendored,
)
@ -45,12 +46,30 @@ def count_lines(path: Path) -> int | None:
return None
def absolutize(path: Path) -> Path:
"""`path` anchored at the cwd, without resolving symlinks.
The exemption rules are written against full paths ('/.claude/skills/'),
but pre-commit passes paths relative to the repo root ('skills/x.md') while
--all walks absolute ones. Matching a relative path against those rules
silently drops the repo-name context, so the same file passed the gate one
way and failed it the other.
Deliberately NOT `Path.resolve()`: build and asset dirs here are symlinks
out to ../testsAndMisc_builds/, and resolving would move those paths
outside the repo and change which exemptions match.
"""
return path if path.is_absolute() else Path.cwd() / path
def exempt_reason(path: Path, lines: int) -> str | None:
"""Why this file is not subject to the cap, or None if it is subject."""
if path.suffix.lower() not in CAPPED_EXTENSIONS:
return "not a capped extension"
if is_vendored(path):
if is_vendored(absolutize(path)):
return "vendored / third-party"
if is_session_artifact(absolutize(path)):
return "frozen session artifact"
if is_generated(path):
return "generated"
try:

View File

@ -7,8 +7,8 @@ is what lets a gate and a survey silently disagree.
from __future__ import annotations
import re
from pathlib import Path
import re
#: Hard cap. Files above this fail the gate. Code and prose alike.
MAX_LINES = 250
@ -158,6 +158,18 @@ VENDORED_SUBPATHS = {
#: repo that wants them, not just the one they were first seen in.
VENDORED_ANYWHERE = ("/.agents/skills/", "/.claude/skills/")
#: Frozen records of past agent sessions: an implementation plan or an
#: append-only session log describes what was done at one moment. Splitting one
#: rewrites a historical record for no reading benefit -- nobody navigates them,
#: they are read whole or not at all. Same category as generated files.
#:
#: Deliberately NOT the whole of docs/superpowers/: `specs/` and `contracts/`
#: stay capped, because those are living documents that get re-read and edited.
SESSION_ARTIFACTS_ANYWHERE = (
"/docs/superpowers/plans/",
"/docs/superpowers/sessions/",
)
#: 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(
@ -205,19 +217,35 @@ def is_data_text(path: Path, lines: int, size: int) -> bool:
def is_vendored(path: Path) -> bool:
"""True if the path sits under a known vendored subtree or excluded dir."""
"""True if the path sits under a known vendored subtree or excluded dir.
Callers must pass an absolute path (see `check.absolutize`). Every rule
below needs the repo-name context that a repo-relative path has already
thrown away: '/.claude/skills/' cannot match 'skills/x.md', so a relative
path would quietly skip the exemption instead of applying it.
"""
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):
if any(marker in posix 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}/"):
if f"/{repo}/{sub}/" in posix:
return True
return any(f"/{repo}/" in posix for repo in THIRD_PARTY_REPOS)
def is_session_artifact(path: Path) -> bool:
"""True for a frozen agent-session record (a plan or a session log).
Kept separate from `is_vendored` so the gate reports why a file is exempt:
these are kuhy's own files, not third-party code.
Callers must pass an absolute path (see `check.absolutize`), for the same
reason `is_vendored` does -- a repo-relative path has already dropped the
leading directory these markers match on.
"""
posix = path.as_posix()
return any(marker in posix for marker in SESSION_ARTIFACTS_ANYWHERE)