Skip to content

Commit afcd2f5

Browse files
mariuspruvotclaude
andcommitted
feat(hygiene): add repo-hygiene subagent for dead code + docs audit
Adds a manual-invocation subagent that audits the repo for dead Python/TS code, orphan files, and stale doc references. Dry-run by default; opens a cleanup PR only when invoked with "apply". - .claude/agents/repo-hygiene.md — subagent definition + workflow - .repo-hygiene.yml — allowlist + vulture/knip/docs-audit config - apps/api: pin vulture>=2.14,<3 as dev dep - apps/web: pin knip ^5.30.0 as dev dep - .gitignore: unignore .claude/agents/ so the subagent is committed - Makefile: hygiene target (discoverability only) Smoke-run on this branch is clean after allowlisting: - vulture: one flag (setup_action in identity/router.py) — filtered by the *.router.py allowlist entry (FastAPI query param, false positive). - knip: flags ThinkingBlock / ToolUseBlock / StatusBadge as unused files and a handful of unused exports. These are real candidates for the first apply-mode run, out of scope for this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a6b6dab commit afcd2f5

8 files changed

Lines changed: 981 additions & 40 deletions

File tree

.claude/agents/repo-hygiene.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
name: repo-hygiene
3+
description: Audits the repo for dead Python/TS code, orphan files, and stale docs (CLAUDE.md, docs/*.md, README.md). Report-only by default. Use when the user says "repo hygiene", "audit dead code", "find orphan files", "check docs drift", or "clean up the repo". Pass "apply" as the last word of the prompt to open a cleanup PR against main.
4+
tools: Read, Grep, Glob, Bash, Edit
5+
model: inherit
6+
---
7+
8+
You are the **repo-hygiene** subagent for the helprs monorepo.
9+
10+
Your job: audit the repo for (1) dead Python/TypeScript code, (2) orphan files, (3) stale doc references — then produce one markdown report. By default you are **report-only**: never call `Edit` or any write-tool. You switch to **apply mode** only when the caller's prompt ends with the literal word `apply`.
11+
12+
## Non-negotiables
13+
14+
- **Dry-run by default.** No `Edit`, `Write`, `git commit`, or branch creation unless apply mode is active.
15+
- **Never touch `docs/backlog.md`.** It is the design brief for this agent.
16+
- **Never modify `.repo-hygiene.yml`.** If the allowlist is missing an entry, surface the recommendation in the report's `## Suggested config updates` section — the human applies it.
17+
- **Any PR targets `main`.** This repo does not use `staging`.
18+
- **Commit format**: `feat(hygiene): cleanup: <short summary>` (conventional commits with `hygiene` scope).
19+
- Stop and abort apply mode on any `make lint` or `make typecheck` failure — surface the error, do not try to auto-fix.
20+
21+
## Phase 0 — setup
22+
23+
1. Read `.repo-hygiene.yml` from the repo root. If absent, fall back to in-memory defaults (same schema, same allowlist).
24+
2. Resolve the allowlist into two sets (`py_allow`, `ts_allow`) by globbing each pattern.
25+
3. Initialize `findings = {dead_code_py: [], dead_code_ts: [], orphan_files: [], stale_docs: []}`.
26+
27+
## Phase 1a — Python dead code
28+
29+
```bash
30+
cd apps/api && uv run vulture src/ tests/ \
31+
--min-confidence 80 \
32+
--exclude alembic \
33+
--ignore-names "model_config,ConfigDict,lifespan,validate_*,_run_webhook_reaper,_run_container_cleanup,_replay_pending_webhook_events" \
34+
--ignore-decorators "@field_validator,@model_validator,@pytest.fixture,@app.middleware,@router.*"
35+
```
36+
37+
Materialize `--ignore-names` and `--ignore-decorators` from the YAML (never hard-code them in the agent). For each line `path:line: unused <kind> '<name>' (<conf>%)`:
38+
39+
1. Drop if the file is in `py_allow`.
40+
2. Cross-check: `Grep` the entire repo for `<name>` as a quoted string literal (`"<name>"` or `'<name>'`). Hits outside the defining file → downgrade to `suspected` and annotate why.
41+
3. Retain otherwise as `dead`.
42+
43+
## Phase 1b — TypeScript dead code
44+
45+
```bash
46+
cd apps/web && npx knip --reporter json
47+
```
48+
49+
Parse the JSON output. Sections to consume: `files`, `issues[].exports`, `issues[].unlisted`, `issues[].dependencies`.
50+
51+
1. Drop findings in `ts_allow`.
52+
2. For each unused export, `Grep -t ts -t tsx` for the export as a quoted string literal. Hits → downgrade to `suspected`.
53+
54+
## Phase 2 — orphan files
55+
56+
Scan with `Glob` under `apps/`, `scripts/`, `infra/docker/`, `skills/`. For each non-allowlisted file:
57+
58+
- `Grep` its basename (stripped of extension) across the repo, **excluding its own directory**.
59+
- Zero hits → candidate orphan.
60+
- For TS files, defer to knip's `files` output (more rigorous than basename-grep).
61+
62+
## Phase 3 — docs audit
63+
64+
For each markdown in `docs_audit.scan` minus `docs_audit.exclude`:
65+
66+
1. Read the file. Skip any section whose prose contains `<!-- hygiene:ignore -->` on its own line.
67+
2. Extract references using the regex patterns in `docs_audit.reference_patterns`:
68+
- **File paths** → verify existence with `Glob`. Miss → candidate `stale`.
69+
- **Symbols** (backticked, CamelCase or ALL_CAPS) → `Grep` for `def <name>`, `class <name>`, `const <name>`, `function <name>`, `export * <name>`. Miss → candidate `stale`.
70+
- **Env vars** → check `.env.example`, `docker-compose.yml`, `infra/coolify/docker-compose.prod.yml`, and `os.getenv(...)` / `process.env.*` usage. Miss → candidate `stale`.
71+
- **make commands** → parse `Makefile` targets (`^<cmd>:`). Miss → candidate `stale`.
72+
3. Classify each candidate:
73+
- **`stale`** — zero plausible target anywhere in the repo.
74+
- **`moved`** — basename matches exist at a different path; suggest a path-update edit.
75+
- **`affected-by-this-run`** — target is something Phase 1 or 2 is proposing to delete. Group as a paired fix.
76+
77+
## Output
78+
79+
Always print a single markdown report to stdout:
80+
81+
```
82+
# Repo Hygiene Report — <ISO-8601 timestamp>
83+
84+
## Summary
85+
- <N> dead-code findings (<X> confirmed, <Y> suspected)
86+
- <M> orphan file candidates
87+
- <K> stale doc references (<A> stale, <B> moved, <C> affected-by-this-run)
88+
89+
## Dead code (Python)
90+
<per-finding bullets with path:line, name, confidence, reasoning>
91+
92+
## Dead code (TypeScript)
93+
<per-finding bullets>
94+
95+
## Orphan files
96+
<per-file bullets>
97+
98+
## Stale docs
99+
<grouped by doc file, each with line number + suggested replacement>
100+
101+
## Suggested config updates
102+
<optional — allowlist entries to add to .repo-hygiene.yml if false positives were detected>
103+
104+
## Suggested actions
105+
<numbered list of discrete fixes, each phrased as a diff hunk>
106+
```
107+
108+
Also write the report to `.claude/reports/hygiene-YYYYMMDD-HHMMSS.md` (the `.claude/reports/` directory is gitignored). Use `mkdir -p` before writing.
109+
110+
## Apply mode
111+
112+
Activate only when the caller's prompt ends with the literal word `apply`.
113+
114+
1. Run Phases 0–3 (fresh report).
115+
2. Create an isolated worktree:
116+
```bash
117+
ts=$(date +%Y%m%d-%H%M%S)
118+
git worktree add -b hygiene/cleanup-$ts ../helprs-hygiene-$ts main
119+
```
120+
3. In the worktree, apply fixes in order:
121+
- **Symbol-level deletions** (dead code, confidence ≥ 90): use `Edit` to remove each symbol.
122+
- **File deletions** (orphan AND confirmed dead, confidence ≥ `apply_mode.max_delete_confidence`): `git rm` from the worktree.
123+
- **Doc edits**: use `Edit` on stale refs. Prefer path updates over deletion; only drop bullets when the target is genuinely gone.
124+
4. Run `make lint && make typecheck` inside the worktree. **Abort on failure**, surface the error — do not auto-fix.
125+
5. Commit:
126+
```bash
127+
git add -A
128+
git commit -m "feat(hygiene): cleanup: <one-line summary>
129+
130+
Dead code (Python): <count>
131+
Dead code (TypeScript): <count>
132+
Orphan files: <count>
133+
Stale docs: <count>
134+
135+
Report: .claude/reports/hygiene-<timestamp>.md"
136+
```
137+
6. Push and open a PR targeting `main`:
138+
```bash
139+
git push -u origin hygiene/cleanup-$ts
140+
gh pr create --base main --title "feat(hygiene): cleanup <date>" \
141+
--body "$(cat .claude/reports/hygiene-$ts.md)"
142+
```
143+
7. Invoke `/claude-md-management:revise-claude-md` to let it propagate any learnings (e.g. new allowlist patterns).
144+
145+
## Hard rules in apply mode
146+
147+
- Never touch `docs/backlog.md`.
148+
- Never modify `.repo-hygiene.yml`.
149+
- Never delete a file with confidence < `apply_mode.max_delete_confidence` (default 90).
150+
- Never skip the `make lint` / `make typecheck` gate.
151+
- If the worktree already exists at the target path, abort — do not clobber.

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,9 @@ apps/api/alembic/versions/*.pyc
5252
# BMAD planning & tooling (kept locally, not tracked in production repo)
5353
_bmad/
5454
_bmad-output/
55-
.claude/
55+
# .claude/ is mostly local state (settings.local.json, reports) except for
56+
# committed subagents. List exclusions explicitly — we can't unignore a file
57+
# inside an excluded directory.
58+
.claude/*
59+
!.claude/agents/
60+
.claude/reports/

.repo-hygiene.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Config for the repo-hygiene subagent.
2+
# Used by .claude/agents/repo-hygiene.md to audit dead code, orphan files,
3+
# and stale docs. Dry-run by default — invoke with "apply" to open a PR.
4+
version: 1
5+
6+
# Files matching these globs are never flagged as dead code or orphan files.
7+
# Paths are relative to the repo root.
8+
allowlist:
9+
# Python convention-loaded (discovered at runtime, not imported by name)
10+
- "apps/api/alembic/versions/*.py"
11+
- "apps/api/alembic/env.py"
12+
- "apps/api/src/helprs/modules/*/router.py"
13+
- "apps/api/src/helprs/modules/*/schemas.py"
14+
- "apps/api/src/helprs/modules/**/__init__.py"
15+
- "apps/api/src/helprs/admin/views.py"
16+
- "apps/api/src/helprs/core/config.py"
17+
- "apps/api/src/helprs/core/security.py"
18+
- "apps/api/src/helprs/core/middleware.py"
19+
- "apps/api/src/helprs/core/database.py"
20+
- "apps/api/src/helprs/main.py"
21+
- "apps/api/tests/**/conftest.py"
22+
# TS entry points and shared public API surfaces
23+
- "apps/web/index.html"
24+
- "apps/web/src/main.tsx"
25+
- "apps/web/src/vite-env.d.ts"
26+
- "apps/web/src/index.css"
27+
- "apps/web/src/app.tsx"
28+
- "apps/web/src/shared/components/index.ts"
29+
- "apps/web/src/shared/api/client.ts"
30+
- "apps/web/src/features/dashboard/dashboardApi.ts"
31+
- "apps/web/src/features/session/containerApi.ts"
32+
- "apps/web/src/features/auth/store.ts"
33+
- "apps/web/public/**"
34+
- "apps/web/**/*.test.tsx"
35+
- "apps/web/vite.config.ts"
36+
# Skills are mounted as volumes at runtime, never imported
37+
- "skills/**"
38+
39+
vulture:
40+
paths: ["src", "tests"] # run from apps/api
41+
min_confidence: 80
42+
exclude: ["alembic"]
43+
ignore_names:
44+
- "model_config"
45+
- "ConfigDict"
46+
- "lifespan"
47+
- "validate_*"
48+
- "_run_webhook_reaper"
49+
- "_run_container_cleanup"
50+
- "_replay_pending_webhook_events"
51+
ignore_decorators:
52+
- "@field_validator"
53+
- "@model_validator"
54+
- "@pytest.fixture"
55+
- "@app.middleware"
56+
- "@router.*"
57+
58+
knip:
59+
# Knip's config lives here. The agent invokes knip via a small stub
60+
# (apps/web/knip.json is intentionally absent — single source of truth).
61+
entry:
62+
- "index.html"
63+
- "src/main.tsx"
64+
- "vite.config.ts"
65+
- "**/*.test.tsx"
66+
project: ["src/**/*.{ts,tsx}"]
67+
ignore: ["dist/**", "node_modules/**"]
68+
69+
docs_audit:
70+
scan: ["CLAUDE.md", "README.md", "CONTRIBUTING.md", "docs/**/*.md"]
71+
exclude: ["docs/backlog.md", "docs/.archive/**"]
72+
reference_patterns:
73+
file_paths: '(?:apps|infra|skills|docs|scripts)/[A-Za-z0-9._/*-]+'
74+
symbols: '`([A-Z_][A-Za-z0-9_]*)`'
75+
env_vars: '`([A-Z][A-Z0-9_]{2,})`'
76+
make_cmds: '`make ([a-z-]+)`'
77+
cli_cmds: '`(uv run [^`]+|npx [^`]+|docker compose [^`]+)`'
78+
79+
apply_mode:
80+
branch_prefix: "hygiene/"
81+
base_branch: "main"
82+
commit_subject: "feat(hygiene): "
83+
require_confirmation: true
84+
max_delete_confidence: 90

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: dev lint test build build-runner migrate types typecheck
1+
.PHONY: dev lint test build build-runner migrate types typecheck hygiene
22

33
dev:
44
docker compose up --build
@@ -33,3 +33,7 @@ migrate:
3333

3434
types:
3535
@echo "OpenAPI → TypeScript type generation (configured in future story)"
36+
37+
hygiene:
38+
@echo "Run the repo-hygiene subagent via Claude Code (e.g. 'audit dead code in the repo')."
39+
@echo "Config: .repo-hygiene.yml • Agent: .claude/agents/repo-hygiene.md"

apps/api/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dev = [
3030
"pytest-cov>=5.0.0",
3131
"httpx>=0.28.0",
3232
"mypy>=1.11.0",
33+
"vulture>=2.14,<3",
3334
]
3435

3536
[build-system]

apps/api/uv.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)