Files
hermes-agent/scripts/check
teknium1 4f70e88c0e fix(code-health): one-to-one move matching, real deadlines, index-exact hook
Second-round review on #132646 (F1-F10), each reproduced on 2531fb2 first:

- Moves are matched one-to-one: unchanged units are reserved first, then
  same-file body, then cross-file body only when the origin's name is gone,
  then same name. A copy of unchanged debt is new debt; a function moved
  over a same-name stub keeps its own cap (F1, F3).
- Every base hit is owned by exactly one head scope, so a file rename no
  longer counts each old hit twice (F2).
- Ruff runs when either side has Python, so a delete-only .py diff with a
  TS edit no longer crashes on a missing ruff result (F4).
- Deadlines must be finite: timeout=None, **{} and asyncio.timeout(None)
  are no deadline; awaited calls inside `async with asyncio.timeout(x)`
  are bounded (F5, F8).
- HX005 walks only what runs at import: the import-time branch of a
  `__name__` guard (either operator), loops, match, try*, decorators,
  class bases; a lambda value is deferred (F9).
- `health: allow` counts only in real comment tokens (Python tokenize, TS
  comment trivia), never in string literals (F7).
- TS function lines are owned by the innermost function, so siblings on
  one line give it up once (F10).
- The pre-commit hook runs `scripts/check --staged`: health compares the
  index tree, and when the working tree differs from the index the other
  checkers run on a checkout-index copy with a copied index, so commits are
  judged on the staged bytes and unstaged work is never touched (F6).
2026-10-04 21:29:39 -07:00

167 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Run every blocking lint check CI runs, locally, with the same pinned tools.
python scripts/check # everything, against the merge-base with origin/main
python scripts/check --only health # just the code-health ratchet
python scripts/check --staged # judge the index (the pre-commit hook runs this)
python scripts/check --install-hook # run it as the git pre-commit hook
CI runs the same command (``.github/workflows/lint.yml``), so a green local run means a green
lint lane. The pre-commit hook is skippable (``git commit --no-verify``); CI is not.
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from collections.abc import Iterator
from concurrent.futures import ThreadPoolExecutor
from contextlib import ExitStack, contextmanager
from dataclasses import dataclass
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO))
from scripts.code_health import gitio # noqa: E402
from scripts.code_health.ruff_runner import resolve_ruff # noqa: E402
_HOOK_MARK = "# hermes-agent scripts/check pre-commit hook"
_HOOK = f"""#!/usr/bin/env bash
{_HOOK_MARK} (installed by `scripts/check --install-hook`; skip once with --no-verify)
repo="$(git rev-parse --show-toplevel)"
# Hooks are shared by every worktree; a branch that predates scripts/check commits unchecked.
[ -f "$repo/scripts/check" ] || exit 0
py=python3
command -v python3 >/dev/null 2>&1 || py=python
exec "$py" "$repo/scripts/check" --staged
"""
# name -> argv (relative to the repo root); mirrors the blocking steps in lint.yml.
_GUARDS = {
"footguns": ["scripts/check-windows-footguns.py", "--all"],
"shebangs": ["scripts/check_bash_shebangs.py"],
"no-tmp": ["scripts/check_no_tmp_literals.py"],
"config-writers": ["scripts/check_config_yaml_writers.py"],
"os-marker-fakes": ["scripts/ci/check_os_marker_fakes.py"],
}
@dataclass
class _Job:
argv: list[str]
cwd: Path = REPO
env: dict[str, str] | None = None
def _run(job: _Job) -> tuple[int, str]:
proc = subprocess.run(
job.argv, cwd=job.cwd, env=job.env, capture_output=True, text=True, encoding="utf-8",
errors="replace", stdin=subprocess.DEVNULL, timeout=900, check=False,
)
return proc.returncode, (proc.stdout + proc.stderr).strip()
def _worktree_matches_index() -> bool:
unstaged = gitio.git(REPO, "diff", "--name-only", "-z").strip("\0")
untracked = gitio.git(REPO, "ls-files", "--others", "--exclude-standard", "-z").strip("\0")
return not unstaged and not untracked
@contextmanager
def _index_snapshot() -> Iterator[tuple[Path, dict[str, str]]]:
"""The staged tree checked out on its own, plus git env that sees exactly that tree.
Unstaged edits stay where they are (no stash, no checkout); the index is read from a copy,
so nothing a checker runs can rewrite the user's real index.
"""
git_dir = Path(gitio.git(REPO, "rev-parse", "--absolute-git-dir").strip())
index = Path(gitio.git(REPO, "rev-parse", "--path-format=absolute", "--git-path", "index").strip())
with tempfile.TemporaryDirectory(prefix="hermes-check-index-") as tmp:
root = Path(tmp) / "tree"
gitio.git(REPO, "checkout-index", "-a", f"--prefix={root}{os.sep}")
index_copy = Path(tmp) / "index"
shutil.copyfile(index, index_copy)
env = {**os.environ, "GIT_DIR": str(git_dir), "GIT_WORK_TREE": str(root),
"GIT_INDEX_FILE": str(index_copy)}
yield root, env
def _jobs(only: str | None, base: str | None, head: str | None,
root: Path = REPO, env: dict[str, str] | None = None) -> dict[str, _Job]:
jobs = {name: _Job([sys.executable, *argv], root, env) for name, argv in _GUARDS.items()}
jobs["ruff"] = _Job([*resolve_ruff(REPO), "check", "."], root, env)
# The ratchet reads both sides from git, so it runs in the real repo against the head tree.
health = [sys.executable, "-m", "scripts.code_health"]
health += ["--base", base] if base else []
health += ["--head", head] if head else []
jobs["health"] = _Job(health)
if only:
return {name: job for name, job in jobs.items() if name in only.split(",")}
return jobs
def _run_all(args: argparse.Namespace) -> dict[str, tuple[int, str]]:
if not args.staged:
jobs = _jobs(args.only, args.base, None)
with ThreadPoolExecutor(max_workers=len(jobs) or 1) as pool:
return dict(zip(jobs, pool.map(_run, jobs.values()), strict=True))
# Pre-commit: judge the bytes being committed, not the working tree.
tree = gitio.git(REPO, "write-tree").strip()
with ExitStack() as stack:
root, env = REPO, None
if not _worktree_matches_index():
root, env = stack.enter_context(_index_snapshot())
jobs = _jobs(args.only, args.base, tree, root, env)
with ThreadPoolExecutor(max_workers=len(jobs) or 1) as pool:
return dict(zip(jobs, pool.map(_run, jobs.values()), strict=True))
def install_hook() -> int:
hooks = Path(gitio.git(REPO, "rev-parse", "--path-format=absolute", "--git-path", "hooks").strip())
target = hooks / "pre-commit"
if target.exists() and _HOOK_MARK not in target.read_text(encoding="utf-8-sig", errors="replace"):
print(f"scripts/check: {target} exists and is not ours; add `python scripts/check` to it")
return 1
hooks.mkdir(parents=True, exist_ok=True)
target.write_text(_HOOK, encoding="utf-8", newline="\n")
os.chmod(target, 0o755) # windows-footgun: ok — chmod +x is a no-op on Windows, harmless
print(f"scripts/check: pre-commit hook installed at {target}")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="scripts/check", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--only", help=f"comma list of: {', '.join([*_GUARDS, 'ruff', 'health'])}")
parser.add_argument("--base", help="base revision for the code-health ratchet")
parser.add_argument("--staged", action="store_true",
help="check the index (what `git commit` records), not the working tree")
parser.add_argument("--install-hook", action="store_true")
args = parser.parse_args(argv)
if args.install_hook:
return install_hook()
try:
results = _run_all(args)
except RuntimeError as exc:
print(f"scripts/check: {exc}", file=sys.stderr)
return 2
failed = [name for name, (code, _) in results.items() if code != 0]
# A passing health run still carries advisories and comment-allowed findings that the
# reviewer must see, so its report is always shown.
for name, (code, output) in results.items():
if code != 0 or name == "health":
label = "failed" if code != 0 else "ok"
print(f"\n=== {name} ({label}) ===\n{output.rstrip()}")
status = "FAILED: " + ", ".join(failed) if failed else "ok"
print(f"\nscripts/check: {len(results)} checks, {status}")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())