git_blame_age
Fire on lines matching a regex whose git blame author-time is older than max_age_days. Same regex match shape as file_content_forbidden, but with a per-line age gate: a TODO added yesterday passes silently; a TODO that has sat in tree for 18 months fires. Closes the gap between level: warning on every TODO (too noisy) and level: off (accepts unbounded debt accumulation).
- id: stale-todos kind: git_blame_age paths: include: ["**/*.{rs,ts,tsx,js,jsx,py,go,java,kt,rb}"] exclude: - "**/*test*/**" - "**/fixtures/**" - "vendor/**" - "third_party/**" pattern: '\b(TODO|FIXME|XXX|HACK)\b' max_age_days: 180 level: warning message: "`{{ctx.match}}` has been here for over 180 days — resolve, convert to a tracked issue, or remove."{{ctx.match}} substitutes the regex capture group 1 when present, otherwise the full match — useful for surfacing which marker was caught (TODO vs FIXME vs …).
Heuristic notes:
- Formatting passes reset blame age.
cargo fmt/prettierrewrites every touched line, attributing it to the format commit rather than the original author. List the formatting-sweep commits in.git-blame-ignore-revsand git applies the right history automatically. - Vendored / imported code carries the import commit’s timestamp — exclude
vendor/,third_party/, generated trees. - Squash-merged PRs collapse to a single commit date, so the squash date wins over the actual edit date.
- Performance.
git blameis O(file_size × commits_touching_file) per file. On large monorepos pair withalint check --changedso blame only runs over modified files in CI.
Outside a git repo, on untracked files, or when blame fails for any other reason, the rule silently no-ops per file. Check-only — auto-removing matched lines is destructive and pinning a line as “do nothing” doesn’t help.