| name | slop-detector |
| description | Crawl a codebase file by file, inspect every method and function for accumulated slop, and produce a detailed YAML report of findings with suggested fixes ready for an executor agent. Use this skill whenever the user asks to find slop, detect code smell, do a code hygiene pass, run a quality sweep, clean up vibe-coded mess, review a codebase for simplification opportunities, or any variant of "find the junk in this code". Also triggers for requests like "what needs cleaning up", "find complexity", "simplify this codebase", "code quality audit" (when not Go-specific), or "review all files for issues". This skill is language-agnostic - it handles Go, TypeScript, Python, Rust, Dart, Bash, HCL, Makefiles, Dockerfiles, and any other language. Do NOT use for single-file reviews, PR reviews, writing new features, or Go-specific audits (use go-code-audit for those). |
Slop Detector - Codebase Hygiene Audit
This skill crawls a codebase file by file, reviews every method and function for accumulated complexity, code smell, and potential bugs, and produces a comprehensive YAML report. The report is designed to be consumed directly by an executor agent that applies the fixes without needing to re-analyse the code.
Mandatory Arguments
Two arguments are required. If either is missing, ask the user before proceeding.
- Base path - the directory to crawl recursively. No default. Example:
./src, ./cmd, ./lib.
- Review mode - one of:
deep - every file gets a full method-by-method review. Use on messy or unfamiliar codebases.
triage - quick first pass per file to decide whether a deep review is warranted. Use on codebases that have already been cleaned up, to catch regressions efficiently.
If the user does not specify both, ask:
I need two things before starting:
- Base path - which directory should I crawl? (e.g.
./src, ./cmd/myapp)
- Review mode -
deep (review every method in every file) or triage (quick scan first, deep review only where needed)?
Output Location
All artifacts go in a timestamped directory:
.project_planning/YYYY-MM-DD-HH-MM-slop-detector/
Create .project_planning/ if it does not exist.
Output Artifacts
Three files are produced:
| File | Purpose |
|---|
report.yaml | Every finding, fully detailed, self-contained. The executor agent's data source. |
progress.yaml | File-level tracking for stop/resume. Content hashes and review status per file. |
summary.md | Human-readable overview. Scan stats, severity breakdown, top offenders, and a full schema reference for report.yaml. This is the entry point for the executor agent. |
Phase 1: Crawl
1.1 Validate arguments
Confirm both base path and review mode are provided. If not, ask.
Verify the base path exists and is a directory. If it does not exist, tell the user and stop.
1.2 Check for a previous run (stop/resume)
Look for an existing .project_planning/*-slop-detector/progress.yaml in reverse chronological order. If one exists with pending or error files:
- Ask the user: "I found an incomplete run from [timestamp] with N files still pending. Resume that run, or start fresh?"
- If resuming: load the existing
progress.yaml, reuse the same output directory, and skip files with status: reviewed whose content hash has not changed.
- If starting fresh: create a new timestamped directory.
1.3 Discover files
git ls-files --cached --others --exclude-standard "<base_path>" 2>/dev/null
find "<base_path>" -type f -not -path '*/.git/*'
1.4 Filter files
Exclude by default (do not review these):
.git/
node_modules/
vendor/
dist/, build/, out/
.project_planning/
- Binary files (images, compiled artifacts, archives)
- Lock files (
package-lock.json, yarn.lock, go.sum, Cargo.lock, poetry.lock)
- Generated code markers: files containing
DO NOT EDIT, auto-generated, Code generated by, or matching patterns like *.gen.ts, *.gen.go, *_generated.go, *.pb.go, *_pb2.py
If in doubt about whether a file is generated, open the first 10 lines and check for generation markers.
1.5 Detect language
Map file extensions to languages. Common mappings:
| Extension(s) | Language |
|---|
.go | Go |
.ts, .tsx | TypeScript |
.js, .jsx | JavaScript |
.py | Python |
.rs | Rust |
.dart | Dart |
.sh, .bash | Bash/Shell |
.tf, .hcl | HCL/Terraform |
.yaml, .yml | YAML |
.json | JSON |
Makefile, *.mk | Make |
Dockerfile, Dockerfile.* | Dockerfile |
.rb | Ruby |
.java, .kt | Java/Kotlin |
.cs | C# |
.c, .h | C |
.cpp, .hpp, .cc | C++ |
.css, .scss, .less | CSS |
.html, .htm | HTML |
.sql | SQL |
.lua | Lua |
.r, .R | R |
.swift | Swift |
.md | Markdown |
.toml | TOML |
For unlisted extensions, record the extension as the language identifier. The model adjusts idiom expectations accordingly.
1.6 Build the progress manifest
For each file that passed filtering, compute a SHA256 hash of its contents and add an entry to progress.yaml:
run_id: "2026-04-30-14-30-slop-detector"
base_path: "./src"
mode: deep
files:
- path: "src/services/auth.ts"
language: typescript
hash: "sha256:abc123..."
status: pending
findings_count: 0
triage_result: null
Write progress.yaml to the output directory immediately.
Phase 2: Review
2.1 Read foundational files first
Before reviewing individual files, read these if they exist at the repo root (or near the base path):
AGENTS.md / CLAUDE.md / CURSOR.md - existing conventions and constraints
README.md - project purpose, architecture
- Build manifest (
package.json, go.mod, Cargo.toml, pyproject.toml, etc.)
- Lint/format config (
.eslintrc, .prettierrc, golangci-lint.yaml, rustfmt.toml, etc.)
These provide context for what the project considers correct. The review should respect existing conventions - flag deviations from the project's own standards, not just textbook ideals.
2.2 Review mode: Deep
Process files one at a time. For each file:
- Read the entire file contents.
- Identify all functions, methods, classes, and meaningful code blocks. Use the file's language semantics - the model understands code structure natively.
- For each function/method/block, evaluate against all seven review criteria (see below).
- Record findings in the structure defined in the Finding Schema section below.
- Update
progress.yaml: set status: reviewed and findings_count.
- Write updated
progress.yaml after each file (not at the end) to support resume.
When reviewing a method, consider the full file context - imports, enclosing class/struct, and other methods in the file. A method that looks odd in isolation might make sense in context (or might not - but check).
2.3 Review mode: Triage
Two-pass process:
Pass 1 - Quick scan: For each file, read the contents and make a rapid assessment:
- Does this file contain anything worth a deep review?
- Look for: long functions (> 40 lines), deep nesting (> 3 levels), obvious duplication, complex conditionals, functions with many parameters, any obvious code smell.
- Record a
triage_result of flagged or clean in progress.yaml.
- For
clean files, set status: skipped with a one-line rationale (e.g. "Small utility file, 3 short functions, all clean").
Pass 2 - Deep review: Apply the full deep review process (2.2) only to files marked flagged.
The triage pass should be fast. Do not analyse each method individually - skim the file structure and make a judgment call. Err on the side of flagging: if unsure, flag it.
Review Criteria
Apply all seven criteria to every method/function/block under review. Not every criterion will produce a finding for every method - only record findings where there is a genuine issue.
Criterion 1: Complexity Reduction
Look for:
- Nested conditionals deeper than 2 levels that could use early returns, guard clauses, or inversion
- Long if/else chains that could be replaced with a map/dictionary lookup, switch statement, or strategy pattern
- Boolean expressions that could be simplified (De Morgan's law, redundant checks, double negation)
- Unnecessary intermediate variables that add indirection without clarity
- Loops that could be replaced with standard library functions (map, filter, reduce, list comprehensions)
- Ternary/conditional expressions nested inside other ternaries
The goal is fewer moving parts. If the same logic can be expressed with fewer branches, fewer variables, or fewer lines without sacrificing readability, that is a finding.
Criterion 2: Extract Method Candidates
Look for:
- Blocks of code within a function that perform a self-contained sub-task. The heuristic: if you would write a comment above a block to explain what it does, that block is a method waiting to be extracted.
- Functions longer than 30-40 lines that contain multiple logical phases (setup, processing, cleanup)
- Repeated inline logic within a single function (same pattern appearing in multiple branches)
- Deeply nested blocks that could become a named helper function, flattening the parent
When suggesting extraction, propose a concrete function name and signature. Be specific about which lines move.
Criterion 3: Language Idiom Alignment
Each language has idiomatic patterns that are more readable and maintainable than generic alternatives. Flag non-idiomatic code and suggest the idiomatic replacement. Examples by language (illustrative, not exhaustive):
- Go: Use
errors.Is/errors.As instead of string matching. Use fmt.Errorf("context: %w", err) for wrapping. Prefer table-driven tests. Use named return values only when they aid documentation, not for bare returns in long functions. Use context.Context as first param.
- TypeScript/JavaScript: Optional chaining (
?.) and nullish coalescing (??) instead of nested null checks. Array.map/filter/reduce instead of manual loops. Destructuring. Template literals instead of concatenation. const by default.
- Python: List/dict/set comprehensions instead of manual loops. Context managers (
with) for resource management. F-strings over .format() or %. Unpacking instead of index access. pathlib over os.path.
- Rust:
? operator instead of match on Result/Option. Iterator chains over manual loops. impl From/Into for type conversions. Destructuring in match arms. derive macros where applicable.
- Dart: Cascade notation (
..). Named parameters for clarity. Null-aware operators (?., ??, ??=). Collection literals. async/await over raw futures.
- Bash: Quote variables. Use
[[ ]] over [ ]. set -euo pipefail. Avoid parsing ls output. Use parameter expansion over sed/awk for simple operations.
Do not flag style preferences that are consistent within the project. If the project consistently uses one valid pattern over another, respect that. Only flag patterns where the non-idiomatic version is genuinely harder to read, maintain, or is error-prone.
Criterion 4: Dead and Unreachable Code
Look for:
- Conditions that can never be true given the control flow (e.g. checking for null after a guard clause already returned)
- Variables assigned but never subsequently read
- Functions defined but never called (and not implementing an interface or exported API)
- Unused imports or dependencies
- Catch/except blocks that only re-raise without modification
- Commented-out code blocks (these should be removed - that is what version control is for)
- Feature flags or conditional blocks that reference permanently stale state
- TODO/FIXME/HACK comments - catalogue these as known debt, do not propose fixes but record them
Criterion 5: Naming
Look for:
- Generic names that convey no meaning:
data, result, tmp, val, item, obj, x, ret, info, stuff
- Inconsistent naming conventions within the same file (mixing camelCase and snake_case, or mixing full words and abbreviations)
- Abbreviations that save keystrokes but cost readability:
cfg vs config is fine, rspBdy is not
- Boolean variables/functions that do not read as predicates (
flag, check vs isValid, hasPermission)
- Names that describe implementation rather than intent (
stringArray vs userNames)
- Single-letter variables outside of very short closures, loop indices, or well-established conventions (e.g.
i, j, k for indices, t for testing.T in Go)
Criterion 6: Method Signature Hygiene
Look for:
- Functions with more than 3-4 parameters - consider grouping into an options/config struct
- Boolean parameters that change behaviour - these should often be separate methods or an enum/constant
- Functions that accept mutable references/pointers where a return value would be clearer
- Inconsistent parameter ordering across related functions in the same file
- Parameters that are always passed together - these are a struct waiting to happen
- Functions that return more than 3 values (in languages that support it) - consider a result struct
Criterion 7: Potential Bugs
Look for:
- Unchecked error returns - especially in Go (
_ = doSomething()) but also unchecked promise rejections in JS/TS, ignored exceptions in Python
- Nil/null/undefined dereference paths - accessing a property on something that could be null without checking first
- Off-by-one errors - boundary conditions in loops, slice operations, string indexing
- Resource leaks - opened files, connections, channels, or handles that are not closed/deferred
- Race conditions - shared mutable state accessed from goroutines/threads/async contexts without synchronisation
- Silent error swallowing - empty catch blocks, errors logged but not returned, fallback values that mask failures
- Type coercion surprises - implicit conversions that could lose data (integer truncation, float precision, string-to-number in JS)
- Incorrect equality checks -
== vs === in JS/TS, reference equality vs value equality
- Missing break/return - switch fallthrough that looks unintentional, missing returns in branches
Do not flag theoretical issues in code that is clearly handling the case correctly. Flag things where the code either has a bug now or will have one when assumptions change.
Phase 3: Report Generation
After all files are reviewed (or the triage+deep passes are complete), generate the three output artifacts.
3.1 report.yaml
Each finding must be self-contained. An executor agent reading a single finding should have everything it needs to apply the change: the file, the method, the line range, the current code, the suggested replacement, and the rationale.
Assign each finding a unique id in the format F001, F002, etc., sequential across the entire report.
Order findings in the report grouped by file path, then by line number within each file. This gives the executor a natural top-to-bottom processing order per file. When applying changes, the executor should process findings within a file in reverse line order (bottom-up) to avoid line number shifts invalidating subsequent findings.
Top-level structure
meta:
run_id: string
base_path: string
mode: string
started_at: string
completed_at: string
files_scanned: integer
files_with_findings: integer
total_findings: integer
findings:
- <finding>
Finding schema
id: string
file: string
language: string
method: string
enclosing: string | null
line_range: [integer, integer]
category: string
severity: string
confidence: string
description: string
current_code: string
suggested_code: string
rationale: string
breaking_risk: string
related_findings: [string]
Example finding
- id: "F012"
file: "src/services/auth.ts"
language: "typescript"
method: "validateToken"
enclosing: "AuthService"
line_range: [45, 92]
category: "complexity_reduction"
severity: "high"
confidence: "high"
description: >
Three levels of nested try/catch with identical error handling in each
branch. The outer conditional on tokenType is a strategy pattern
candidate - each branch does the same thing with a different validator.
The duplicated catch blocks mean error handling changes need to be made
in three places.
current_code: |
if (tokenType === 'jwt') {
try {
const decoded = this.jwtVerify(token);
return { valid: true, claims: decoded };
} catch (e) {
logger.error('Token validation failed', e);
return { valid: false, error: e.message };
}
} else if (tokenType === 'opaque') {
// ... same pattern repeated for opaque and api-key ...
}
suggested_code: |
const validators: Record<string, (token: string) => Promise<unknown>> = {
jwt: (t) => this.jwtVerify(t),
opaque: (t) => this.opaqueCheck(t),
'api-key': (t) => this.apiKeyLookup(t),
};
const validator = validators[tokenType];
if (!validator) {
return { valid: false, error: `Unsupported token type: ${tokenType}` };
}
try {
const claims = await validator(token);
return { valid: true, claims };
} catch (e) {
logger.error('Token validation failed', e);
return { valid: false, error: e.message };
}
rationale: >
Eliminates three duplicate catch blocks and two levels of nesting;
adding a new token type becomes a one-line map entry.
breaking_risk: "low"
related_findings: []
Executor agent notes
- Each finding is self-contained. The
current_code and suggested_code fields provide everything needed.
- Check
related_findings before applying. If two findings touch overlapping lines, apply together or in correct order.
- Check
breaking_risk. For medium or high, run the test suite after applying and before committing.
line_range refers to the original file. After applying a previous finding to the same file, line numbers may have shifted. Locate current_code by content matching, not line numbers alone.
suggested_code may need indentation adjustment to match surrounding context.
3.2 progress.yaml
This should already be up to date from the review phase (it is written after each file). Do a final pass to ensure all statuses are correct and findings_count values match the report.
3.3 summary.md
Write summary.md using this template:
# Slop Detector Report: [base_path]
## Run Details
- **Date**: YYYY-MM-DD HH:MM
- **Base path**: [base_path]
- **Mode**: [deep | triage]
- **Files scanned**: N
- **Files with findings**: N
- **Files skipped (triage)**: N (triage mode only)
- **Total findings**: N
## Severity Breakdown
| Severity | Count |
|----------|-------|
| High | N |
| Medium | N |
| Low | N |
## Category Breakdown
| Category | Count |
|------------------------|-------|
| Complexity reduction | N |
| Extract method | N |
| Language idioms | N |
| Dead/unreachable code | N |
| Naming | N |
| Signature hygiene | N |
| Potential bugs | N |
## Top Offenders
[List the 10 files with the most findings, sorted by count descending. Include finding count and a one-line summary of the dominant issue type.]
## High-Severity Findings
[List all high-severity findings here as a quick reference, with finding ID, file, method, category, and a one-sentence description. Potential bugs with high severity should appear first.]
## Executor Instructions
This section is the entry point for an executor agent. The companion file `report.yaml` in this directory contains every finding with full detail. Read this section in full before beginning work.
### What report.yaml contains
Each entry in `report.yaml` is a self-contained finding with these fields:
- `id` - unique identifier (F001, F002, ...)
- `file` - relative path to the source file
- `language` - the file's language
- `method` - the function/method the finding applies to
- `enclosing` - the class/struct/module containing the method (null if free function)
- `line_range` - [start, end] line numbers in the original file at scan time
- `category` - one of: complexity_reduction, extract_method, language_idioms, dead_code, naming, signature_hygiene, potential_bug
- `severity` - high, medium, or low
- `confidence` - high or medium. Medium-confidence findings are still worth applying but deserve extra scrutiny - read the description carefully as it may note alternative approaches or flag a design question.
- `description` - what the problem is and why it matters. For medium-confidence findings, may describe alternative approaches or frame a design decision that needs human input.
- `current_code` - the exact source code that needs changing (verbatim from the file)
- `suggested_code` - the proposed replacement, ready to paste. Empty string for catalogue-only findings (TODOs, known debt) that need no code change. For design-decision findings, contains the more defensive/explicit option - read the description for context before applying.
- `rationale` - one sentence on why the change is better
- `breaking_risk` - low (safe), medium (verify with tests), or high (may change behaviour)
- `related_findings` - IDs of other findings that interact with this one
### Processing order
1. Process findings grouped by file. All findings for one file should be applied before moving to the next file.
2. Within each file, apply findings in **reverse line order** (highest `line_range` first, working upward). This prevents earlier changes from shifting line numbers and invalidating later findings.
3. Process high-severity findings before medium, and medium before low, when choosing which files to tackle first. The Top Offenders and High-Severity Findings sections above give a starting point.
### How to apply each finding
For each finding:
1. Open the file at `file`.
2. Locate the `current_code` block by **content matching**, not by `line_range`. Line numbers are a hint for initial orientation but may have drifted if previous findings in the same file have already been applied.
3. If `suggested_code` is empty, the finding is informational (e.g. cataloguing a TODO). Skip it - no code change needed.
4. Replace the matched `current_code` with `suggested_code`. Adjust indentation to match the surrounding context if needed.
5. If `related_findings` is non-empty, read those findings before applying. They may touch overlapping lines or depend on each other. Apply related findings together or in the correct dependency order.
### Verification and commits
- After applying all findings in a single file, verify the project still builds/compiles. If the project has a build command (e.g. `go build ./...`, `tsc --noEmit`, `cargo check`), run it.
- For findings with `breaking_risk: medium` or `breaking_risk: high`, run the project's test suite after applying and before committing. If tests fail, revert the change and skip the finding - do not attempt to fix tests to accommodate the change.
- Commit after each file is complete (all findings applied and verified). Use a commit message in the format: `refactor(<scope>): apply slop-detector findings F001-F00N` where scope is the file or module name.
- If a `current_code` block cannot be located in the file (the code has already been changed or moved), skip the finding and note it.
### What NOT to do
- Do not re-analyse the code or second-guess findings. The analysis is done. Your job is to apply the suggested changes mechanically.
- Do not apply findings with empty `suggested_code` - these are informational only.
- Do not modify code beyond what `suggested_code` specifies. If the surrounding code also looks improvable, that is a separate concern for a future audit.
- Do not apply a finding if it would break the build or fail tests. Skip it and move on.
- For findings where the description frames a design question (e.g. "this may be intentional best-effort behaviour"), do not resolve the question yourself. Apply the suggested code if `breaking_risk` is `low`, otherwise skip and leave the finding for human review.
## Report Schema Reference
[Reproduce the full Finding Schema from the skill document here: all field names, types, enum values, and descriptions. This must be comprehensive enough that the executor agent can parse report.yaml without ambiguity.]
## Notes
[Any caveats, observations about the codebase, or recommendations that do not fit into individual findings. For example: "The project has no linter configuration - many idiom findings would be caught automatically by enabling ESLint/golangci-lint." Or: "Several files appear to be machine-generated but lack generation markers - verified manually that they are hand-written."]
Important Guidelines
Inclusion posture: when in doubt, include
Under-reporting is worse than over-reporting. A missed finding is invisible; a low-confidence finding is easily skipped by the executor. When uncertain whether something is worth reporting, include it with honest confidence and breaking_risk calibration and let the downstream consumer decide.
What counts as noise (and what does not)
Noise means trivial formatting preferences, cosmetic naming nitpicks on names that are already acceptable, and purely subjective style choices where both alternatives are equally readable. If a method is clean, record zero findings - do not invent problems.
The following are never noise regardless of confidence level:
- Silent error swallowing, unchecked returns, or dropped failures - these are always findings, even if the current behaviour might be intentional. Report them with a description that frames the design question clearly.
- Structural debt and extract-method candidates in large functions, even when multiple valid decompositions exist.
- Architectural concerns like shared mutable global state, even if "only one instance exists in practice today."
- Any pattern that would cause a bug if assumptions change (e.g. a function that works only because callers happen to pass non-nil values today).
Suggested code is a strong default, not a gate
Every finding should include a suggested_code field where possible. However, the inability to produce a single unambiguous replacement must not prevent a finding from being reported. When multiple valid approaches exist, pick the most likely one, describe the alternatives in the description field, and set confidence: medium. When the fix depends on a product or API decision the auditor cannot resolve, describe both options in description, provide the suggested code for the more conservative option, and set breaking_risk: medium or breaking_risk: high. For catalogue-only findings (TODOs, known debt) that genuinely need no code change, set suggested_code to an empty string.
Findings that need a design decision
Some findings surface a question rather than a clear defect - "should grep be best-effort or fail loudly on unreadable files?" These are valuable precisely because they expose undocumented behaviour contracts. Report them as findings (usually potential_bug or complexity_reduction category) with:
- A description that frames the design question clearly, stating what the code does now and what the alternative behaviour would be.
suggested_code implementing the more defensive/explicit option (e.g. surfacing the error rather than swallowing it).
confidence: medium to signal that the current behaviour may be intentional.
breaking_risk: medium or breaking_risk: high as appropriate.
The executor or the human reviewing the summary can decide whether to act. The slop detector's job is to surface these, not to resolve product questions.
Other guidelines
- Never modify source code. This skill produces a report. It does not change any files in the target codebase. The executor agent handles implementation.
- Be concrete. "Consider simplifying this" is not a finding. "Replace the nested if/else on lines 45-67 with a map lookup" is a finding. Every finding must include the specific code and the specific issue, even if the suggested fix is one of several valid approaches.
- Respect existing conventions. If the project consistently does something one way, flag it only if it is genuinely problematic. Do not impose textbook style on a codebase with its own coherent conventions. Note "this deviates from [standard] but is internally consistent - recommend aligning, user should confirm" when appropriate.
- Calibrate severity honestly.
high means "this is actively harmful - it will cause bugs, makes the code hard to maintain, or hides errors". medium means "this should be fixed but is not causing immediate harm". low means "polish - improves readability or consistency".
- Calibrate confidence honestly.
high means "this is clearly an issue and the suggested fix is clearly correct". medium means "this looks like an issue but there may be context I'm missing, or the suggested fix is one of several valid approaches". medium is not a reason to exclude a finding.
- Preserve behaviour in suggested code. Every
suggested_code should preserve the external behaviour of the code where possible. If a change would alter semantics, set breaking_risk accordingly and explain what changes in the description. This constraint applies to the suggested fix, not to whether the finding is reported.
- Cross-reference related findings. If fixing finding F003 would conflict with or be affected by finding F007, link them via
related_findings. The executor needs to know about ordering dependencies.
- Write progress.yaml after every file. This is the resume mechanism. If the run is interrupted after reviewing 30 of 50 files, the next run picks up at file 31.