Simplifying Code
Principles
| Principle |
Rule |
| Preserve behavior |
Output must do exactly what the input did -- no silent feature additions or removals. Specifically preserve: async/sync boundaries (do not convert sync to async or reverse), error propagation paths (do not alter strategy), logging/telemetry/guards/retries that encode operational intent, and domain-specific steps (do not collapse into generic helpers that hide intent) |
| Explicit over clever |
Prefer explicit variables over nested expressions. Readable beats compact |
| Simplicity over cleanliness |
Prefer straightforward code over pattern-heavy "clean" code. Three similar lines beat a premature abstraction |
| Surgical changes |
Touch only what needs simplifying. Match existing style, naming conventions, and formatting of the surrounding code |
| Surface assumptions |
Before changing a block, identify what imports it, what it imports, and what tests cover it. Edit dependents in the same pass |
Process
- Read first -- understand the full file and its dependents before changing anything. Apply Chesterton's Fence: if you see code that looks unnecessary but don't understand why it's there, check
git blame before removing it. First understand the reason, then decide if the reason still applies.
- Identify invariants -- what must stay the same? Public API, return types, side effects, error behavior
- Identify targets -- find the highest-impact simplification opportunities. Impact = readability and maintainability; prioritize: control flow -> naming -> duplication -> types (see Smell -> Fix table)
- Apply in order -- control flow → naming → duplication → data shaping → types. Structural changes first, cosmetic last
- Verify -- confirm no behavior change: tests pass, types check, imports resolve
- Pre-submit scope audit -- walk every changed line and ask "does the requested task explicitly require this line?" If no, revert it and list it as a follow-up under Residual Risks. Drive-by edits belong in a separate change, not the current patch. For the pre-edit complement on ambiguous-scope requests ("simplify my project"), see
ia-verification-before-completion's Scope Confirmation gate.
Smell → Fix
| Smell |
Fix |
| Deep nesting (>2 levels) |
Guard clauses with early returns |
| Long function (>20 lines) |
Extract into named functions by responsibility |
| Too many parameters (>3) |
Group into an options/config object |
| Duplicated block (3+ occurrences) |
Extract shared function. Two copies = leave inline; wait for the third |
| Magic numbers/strings |
Named constants |
| Complex conditional |
Extract to descriptively-named boolean or function |
| Dense transform chain (3+ chained methods) |
Break into named intermediates for debuggability |
| Dead code / unreachable branches |
Delete entirely -- no commented-out code |
Unnecessary else after return |
Remove else, dedent |
AI Slop Removal
When simplifying AI-generated code, specifically target:
- Redundant comments that restate the code (
// increment counter above counter++) -- delete them
- Unnecessary defensive checks for conditions that cannot occur in context -- remove the guard
- Gratuitous type casts (
as any, as unknown as T) -- fix the actual type or use a proper generic
- Over-abstraction (factory for 2 objects, wrapper around a single call, util file with 1 function) -- inline the code
- Inconsistent style that drifts from the file's existing conventions -- match the file
- Placeholder stubs (
// ..., // rest of code, // similar to above, // continue pattern, // add more as needed) -- leave unsimplified code as-is rather than replacing it with stubs
- Redundant error wrapping (
catch(e) { throw e; }, catch(e) { throw new Error(e.message); }) that strips the original stack for no reason -- remove the try/catch entirely and let errors propagate
- Verbose stdlib reimplementations (hand-rolled loops that replicate
array_filter, Array.from, Collection::pluck(), itertools) -- replace with the stdlib/framework one-liner
Stop Conditions
Stop and ask before proceeding when:
- Simplification requires changing a public API (function signatures, return types, exports)
- Behavior parity cannot be verified (no tests exist and behavior is non-obvious)
- Code is intentionally complex for domain reasons (performance-critical, protocol compliance)
- Scope implies a redesign rather than a simplification
Constraints
- Only simplify what was requested -- do not add features, expand scope, or introduce new dependencies
- Leave unchanged code untouched -- do not add comments, docstrings, or type annotations to lines that were not simplified
- Do not bundle unrelated cleanups into one patch -- each simplification should be a coherent, reviewable unit
- Do not introduce framework-wide patterns while simplifying a small local change
- Do not replace understandable duplication with opaque utility layers -- three similar lines are better than a premature abstraction
- Keep comments that explain intent, invariants, or non-obvious constraints. Remove comments that restate obvious code behavior.
- If a simplification would make the code harder to understand, skip it
- Watch for over-simplification: inlining too aggressively removes names that gave concepts meaning; combining unrelated logic into one function hides distinct responsibilities; removing abstractions that exist for testability breaks the test suite
- When unsure whether a block is dead code, ask instead of deleting
Verify
- Tests pass and types check after changes
- No behavior change (same inputs produce same outputs)
- Scope limited to requested files -- no drive-by cleanups
- Match test scope to the importer count surfaced in step 1 (Surface assumptions). Zero external importers: scoped tests on the changed paths. One or more external importers, or shared/utility code edited: run tests covering each importer. Run the full suite when the test runner has no path-scoping mechanism.
Orchestrator Mode (When Chained With Other Skills)
When this skill is invoked by an orchestrator that also runs ia-code-review, ia-writing-tests, or ia-verification-before-completion on the same scope, each sub-skill re-resolving scope independently wastes tokens and risks drift. Avoid this by resolving scope exactly once and passing a canonical block to every sub-skill.
Resolved scope format — the orchestrator builds this once, before dispatching any sub-skill:
## Resolved scope
Files:
- path/to/file-a.ts
- path/to/file-b.ts
Commit range: HEAD~3..HEAD (or "uncommitted")
Intent: [one-sentence description pulled from the user request or PR description]
Constraints:
- Preserve public API
- No behavior change
- [other constraints specific to this run]
Every chained sub-skill receives this block verbatim in its prompt and uses it as the source of truth — no re-running git diff --name-only, no re-parsing the user request, no independent scope resolution. Sub-skills accept --no-verify --no-report flags when chained so verification and reporting happen once at the end of the chain, not per-skill. The last sub-skill in the chain runs verification; the orchestrator trusts that result rather than re-verifying.
This prevents two failure modes: scope drift (sub-skill A simplifies one set of files, sub-skill B reviews a different set) and double work (every sub-skill rediscovers the same facts).
Integration
ia-code-simplicity-reviewer agent -- analysis-only pass producing a simplification report (no code changes). Use before refactoring to identify targets.
Output
After simplifying, report:
- Scope touched: files and functions modified
- Key simplifications: what changed and why (one line each)
- Verification: tests pass, types check, no behavior change
- Residual risks: assumptions made, areas not touched that may need attention
1---2name: simplifying-code3description: Simplifies, polishes, and declutters code without changing behavior. Use when asked to simplify, clean up, refactor, declutter, remove dead code or AI slop, or improve readability. For analysis-only reports without code changes, use code-simplicity-reviewer agent.4---56# Simplifying Code78## Principles910| Principle | Rule |11|-----------|------|12| **Preserve behavior** | Output must do exactly what the input did -- no silent feature additions or removals. Specifically preserve: async/sync boundaries (do not convert sync to async or reverse), error propagation paths (do not alter strategy), logging/telemetry/guards/retries that encode operational intent, and domain-specific steps (do not collapse into generic helpers that hide intent) |13| **Explicit over clever** | Prefer explicit variables over nested expressions. Readable beats compact |14| **Simplicity over cleanliness** | Prefer straightforward code over pattern-heavy "clean" code. Three similar lines beat a premature abstraction |15| **Surgical changes** | Touch only what needs simplifying. Match existing style, naming conventions, and formatting of the surrounding code |16| **Surface assumptions** | Before changing a block, identify what imports it, what it imports, and what tests cover it. Edit dependents in the same pass |1718## Process19201. **Read first** -- understand the full file and its dependents before changing anything. Apply Chesterton's Fence: if you see code that looks unnecessary but don't understand why it's there, check `git blame` before removing it. First understand the reason, then decide if the reason still applies.212. **Identify invariants** -- what must stay the same? Public API, return types, side effects, error behavior223. **Identify targets** -- find the highest-impact simplification opportunities. Impact = readability and maintainability; prioritize: control flow -> naming -> duplication -> types (see Smell -> Fix table)234. **Apply in order** -- control flow → naming → duplication → data shaping → types. Structural changes first, cosmetic last245. **Verify** -- confirm no behavior change: tests pass, types check, imports resolve256. **Pre-submit scope audit** -- walk every changed line and ask "does the requested task explicitly require this line?" If no, revert it and list it as a follow-up under Residual Risks. Drive-by edits belong in a separate change, not the current patch. For the pre-edit complement on ambiguous-scope requests ("simplify my project"), see `ia-verification-before-completion`'s Scope Confirmation gate.2627## Smell → Fix2829| Smell | Fix |30|-------|-----|31| Deep nesting (>2 levels) | Guard clauses with early returns |32| Long function (>20 lines) | Extract into named functions by responsibility |33| Too many parameters (>3) | Group into an options/config object |34| Duplicated block (**3+** occurrences) | Extract shared function. Two copies = leave inline; wait for the third |35| Magic numbers/strings | Named constants |36| Complex conditional | Extract to descriptively-named boolean or function |37| Dense transform chain (3+ chained methods) | Break into named intermediates for debuggability |38| Dead code / unreachable branches | Delete entirely -- no commented-out code |39| Unnecessary `else` after return | Remove `else`, dedent |4041## AI Slop Removal4243When simplifying AI-generated code, specifically target:4445- **Redundant comments** that restate the code (`// increment counter` above `counter++`) -- delete them46- **Unnecessary defensive checks** for conditions that cannot occur in context -- remove the guard47- **Gratuitous type casts** (`as any`, `as unknown as T`) -- fix the actual type or use a proper generic48- **Over-abstraction** (factory for 2 objects, wrapper around a single call, util file with 1 function) -- inline the code49- **Inconsistent style** that drifts from the file's existing conventions -- match the file50- **Placeholder stubs** (`// ...`, `// rest of code`, `// similar to above`, `// continue pattern`, `// add more as needed`) -- leave unsimplified code as-is rather than replacing it with stubs51- **Redundant error wrapping** (`catch(e) { throw e; }`, `catch(e) { throw new Error(e.message); }`) that strips the original stack for no reason -- remove the try/catch entirely and let errors propagate52- **Verbose stdlib reimplementations** (hand-rolled loops that replicate `array_filter`, `Array.from`, `Collection::pluck()`, `itertools`) -- replace with the stdlib/framework one-liner5354## Stop Conditions5556Stop and ask before proceeding when:57- Simplification requires changing a public API (function signatures, return types, exports)58- Behavior parity cannot be verified (no tests exist and behavior is non-obvious)59- Code is intentionally complex for domain reasons (performance-critical, protocol compliance)60- Scope implies a redesign rather than a simplification6162## Constraints6364- Only simplify what was requested -- do not add features, expand scope, or introduce new dependencies65- Leave unchanged code untouched -- do not add comments, docstrings, or type annotations to lines that were not simplified66- Do not bundle unrelated cleanups into one patch -- each simplification should be a coherent, reviewable unit67- Do not introduce framework-wide patterns while simplifying a small local change68- Do not replace understandable duplication with opaque utility layers -- three similar lines are better than a premature abstraction69- Keep comments that explain intent, invariants, or non-obvious constraints. Remove comments that restate obvious code behavior.70- If a simplification would make the code harder to understand, skip it71- Watch for over-simplification: inlining too aggressively removes names that gave concepts meaning; combining unrelated logic into one function hides distinct responsibilities; removing abstractions that exist for testability breaks the test suite72- When unsure whether a block is dead code, ask instead of deleting7374## Verify7576- Tests pass and types check after changes77- No behavior change (same inputs produce same outputs)78- Scope limited to requested files -- no drive-by cleanups79- Match test scope to the importer count surfaced in step 1 (Surface assumptions). Zero external importers: scoped tests on the changed paths. One or more external importers, or shared/utility code edited: run tests covering each importer. Run the full suite when the test runner has no path-scoping mechanism.8081## Orchestrator Mode (When Chained With Other Skills)8283When this skill is invoked by an orchestrator that also runs `ia-code-review`, `ia-writing-tests`, or `ia-verification-before-completion` on the same scope, each sub-skill re-resolving scope independently wastes tokens and risks drift. Avoid this by resolving scope exactly once and passing a canonical block to every sub-skill.8485**Resolved scope format** — the orchestrator builds this once, before dispatching any sub-skill:8687```88## Resolved scope89Files:90- path/to/file-a.ts91- path/to/file-b.ts9293Commit range: HEAD~3..HEAD (or "uncommitted")9495Intent: [one-sentence description pulled from the user request or PR description]9697Constraints:98- Preserve public API99- No behavior change100- [other constraints specific to this run]101```102103Every chained sub-skill receives this block verbatim in its prompt and uses it as the source of truth — no re-running `git diff --name-only`, no re-parsing the user request, no independent scope resolution. Sub-skills accept `--no-verify --no-report` flags when chained so verification and reporting happen once at the end of the chain, not per-skill. The last sub-skill in the chain runs verification; the orchestrator trusts that result rather than re-verifying.104105This prevents two failure modes: scope drift (sub-skill A simplifies one set of files, sub-skill B reviews a different set) and double work (every sub-skill rediscovers the same facts).106107## Integration108109- `ia-code-simplicity-reviewer` agent -- analysis-only pass producing a simplification report (no code changes). Use before refactoring to identify targets.110111## Output112113After simplifying, report:114- **Scope touched**: files and functions modified115- **Key simplifications**: what changed and why (one line each)116- **Verification**: tests pass, types check, no behavior change117- **Residual risks**: assumptions made, areas not touched that may need attention