SuperAPI AI Directory

repo-hygiene

Coding & development

View repository

Updated 10-15 11:11

About this skill

Repo hygiene

Every check here is decided by the filesystem and by git. No source file is read, and no symbol is understood. That is the whole membership rule: a check that needs to know what a symbol is for, or whether a reference reaches it, does not belong in this catalog and belongs to senior-review:cleanup-auditor instead.

Two profiles over one catalog

The checks are defined once. Two profiles decide the perimeter, and nothing else differs between them. A caller names its profile; a check that has no meaning at the chosen perimeter is skipped rather than silently widened.

full lite
Perimeter The whole working tree Only files the diff ADDS
Callers /repo-hygiene:tidy, repo-hygiene:workspace-auditor The inline hygiene pass of /senior-review:code-review and /senior-review:pr-review
C1 filesystem garbage yes yes, restricted to added paths
C2 generated artifacts tracked yes yes, restricted to added paths
C3 .gitignore completeness yes only for the ecosystems the added paths belong to
C4 .gitignore archaeology yes no
C5 scratch directories yes no
C6 orphan doc-assets yes no
C7 git auxiliary state yes no

The lite profile cannot widen. C4 through C7 are repository-historical by construction: an ignore rule stale since 2024, a scratch directory nobody touched, a stash from last month. None of them can be caused by the diff under review, so reporting them there is noise attributed to an innocent change. A lite caller that finds itself listing a Python ignore rule on a pull request that added one TypeScript file has widened, and that is a defect.

The catalog

C1 Filesystem garbage

git ls-files 2>/dev/null | grep -iE '(^|/)(nul|\.DS_Store|Thumbs\.db|desktop\.ini|\.swp|\.swo|#.*#|~$)|(^|/)[<>|&]|2>&1'

Shell-redirection artifacts (nul, files named 2>&1 or starting with >) are the highest-confidence class in this catalog: nothing legitimately produces them.

C2 Generated artifacts tracked in git

git ls-files 2>/dev/null | grep -E '(^|/)(dist|build|out|\.next|\.nuxt|\.cache|\.turbo|\.parcel-cache|__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache|\.venv|venv|node_modules|target|\.gradle|android/app/build|ios/build|src-tauri/gen|src-tauri/target)(/|$)'
git ls-files 2>/dev/null | grep -E '\.(bundle\.js|chunk\.js|min\.js\.map|pyc|class|o|obj)$'

A tracked build output is not automatically a mistake. Repositories commit dist/ on purpose: GitHub Pages serves from it, a generated SDK ships to package consumers, a vendored bundle exists so installs need no build step. Untracking those breaks the next clean checkout while every local command keeps passing, because the files are still on disk. Before flagging, check for a publication convention that requires the path:

ls .github/workflows/*pages* .nojekyll CNAME 2>/dev/null      # Pages publication
git show HEAD:package.json 2>/dev/null | grep -A5 '"files"'   # npm publish allowlist
grep -rl "$(basename <path>)" --include='*.yml' .github/ 2>/dev/null

A hit means the artifact is intentional. Report it as KEEP with the convention named, never as a removal candidate.

C3 .gitignore completeness

Detect project signals, then check the corresponding patterns exist:

Signal Expected patterns
package.json node_modules/, dist/, build/, .env, .env.local, npm-debug.log*
vite.config.* dist/, .vite/
next.config.* .next/, out/
src-tauri/ src-tauri/target/, src-tauri/gen/
Cargo.toml target/, and Cargo.lock only for libraries
pyproject.toml or requirements.txt __pycache__/, *.pyc, .venv/, *.egg-info/, .pytest_cache/
*.gradle android/app/build/, android/build/, android/.gradle/
Podfile ios/build/, ios/Pods/
any .DS_Store, Thumbs.db

A missing pattern with a tracked file matching it is a .gitignore gap.

C4 .gitignore archaeology

Establish provenance before judging anything:

git check-ignore -v <path>

It names the source file and line: .gitignore, a nested ignore file, .git/info/exclude, or the global ignore. Only rules in tracked ignore files become findings. Local and global sources get a note at most, because they are not the repository's to change.

  • Stale rule: a pattern matching nothing on disk and nothing plausible in recent history. LOW. Never flag prophylactic ecosystem defaults (node_modules/, __pycache__/, .DS_Store): their value is precisely that they match nothing yet.
  • Overly-broad rule: a pattern whose ignored matches include source- or config-shaped files outside generated directories. Action UNIGNORE. Verify each candidate is not generated before flagging.

C5 Scratch and pipeline-output directories

for d in .upstream-scratch .codebase-xray .daodan .team-review .codebase-map .research .brainstorm \
         .peer-review .frontend-review tmp temp scratch _wip wip _drafts; do
  [ -d "$d" ] && echo "$d"
done
ls NOTES.md TODO.md SCRATCH.md 2>/dev/null

Protected protocol roots: never propose removing .daodan/ or .codebase-xray/. Also protect any directory containing a .daodan-root sentinel and its ancestors. Recognize these by name/sentinel presence only. Do not parse work.json, classify runs or infer retention eligibility. Lifecycle consolidation owns that decision. Apply this exclusion before the ordinary three states below.

Three states, three dispositions:

  • Untracked and already ignored: local clutter only. LOW. Protected roots are excluded. Other untracked candidates can only move to quarantine under the accepted tidy plan.
  • Tracked: clutters every clone. HIGH, git rm -r plus an ignore entry.
  • Tracked and already ignored: an impossible state that means the file was committed before the pattern existed. Sanity-check before acting.

C6 Orphan doc-assets

List images and diagrams under docs/, docs/images/, docs/assets/, docs/diagrams/, .github/assets/, then Grep each basename across **/*.md, **/*.mdx, **/*.rst, **/*.adoc.

Zero literal references is not proof of orphanhood. A documentation asset reaches the rendered site through paths a basename Grep never sees: a value in mkdocs.yml or docusaurus.config.js, a url() in a stylesheet, a generated navigation entry, a path composed in a template. Widen the search before believing the result:

grep -rl "$(basename <asset>)" --include='*.yml' --include='*.yaml' --include='*.toml' \
  --include='*.json' --include='*.css' --include='*.scss' --include='*.html' --include='*.j2' .
grep -rlE "(logo|banner|icon|favicon|social)" mkdocs.yml docusaurus.config.* _config.yml 2>/dev/null

An asset that survives both searches is a candidate, never a confirmed orphan. Removal requires item-level approval, and the finding says so.

C7 Git auxiliary state

git stash list
git worktree list
git branch -vv | grep ': gone]'
git branch --merged

Stashes idle beyond 90 days, worktrees pointing at deleted branches or paths, local branches whose upstream is gone, and merged-but-undeleted branches are LOW to MEDIUM findings.

This dimension is detection-only, permanently, and the reason is structural. Every other check in this catalog mutates tracked content, so a commit records what changed and reverting that commit restores it. These four do not. A dropped stash leaves no trace in any commit; a removed worktree with uncommitted files inside takes them with it; a deleted branch's tip survives only in a reflog that expires. Per-phase commits cannot be the rollback mechanism for a mutation that produces no diff, so nothing here is applied automatically at any confirmation level. Removal commands belong in the finding text, for the user to run and own.

Finding format

Every finding carries the same six fields, whatever the profile:

[C<n>] <one-line description>
  path:        <path or ref>
  evidence:    <the command output that decided it>
  disposition: KEEP | KEEP+IGNORE | REMOVE | REMOVE+IGNORE | UNIGNORE | REVIEW | REPORT-ONLY
  confidence:  HIGH | MEDIUM | LOW
  phase:       garbage | gitignore | scratch | git-state

phase names the /repo-hygiene:tidy phase that resolves it. It never names a senior-review phase: the two commands own disjoint sets, and a finding that crosses that line is misfiled rather than merely mislabelled.

Application safety

What the catalog permits, by disposition. A caller that only detects ignores this section entirely.

Disposition Applied how
REMOVE on untracked files Moved to .repo-hygiene/quarantine/<timestamp>/, never deleted. Git holds no copy of an untracked file, so deletion is the one irreversible operation available and it is not taken.
REMOVE on tracked files git rm, recoverable from history.
KEEP+IGNORE, REMOVE+IGNORE Append the pattern, then git rm --cached per item, each confirmed, because C2's publication check is a heuristic and being wrong breaks a clean checkout while every local signal stays green.
UNIGNORE Edit the ignore rule only. Never git add on the user's behalf.
REVIEW, REPORT-ONLY Never applied. Reported with the command the user would run.