agentleFS
dekozadekoza/oh-my-slop/skills/practice/legacy-code/SKILL.md

legacy-code

Skill4 starsChanged 8 months agoUnlicenseView on GitHub

Changing code that is expensive to change safely because behavior is unclear, tests are weak or missing, dependencies are hidden, or runtime/framework setup blocks local feedback. Based on Working Effectively with Legacy Code (Michael Feathers). Covers: characterization tests, seams, dependency breaking, sprout/wrap techniques, incremental risk reduction. Triggers on: "legacy code", "untested code", "change existing code safely", "refactor legacy", "add tests to legacy code", "break dependencies", "no tests", "unclear behavior", "expensive to change", or when touching code with no tests or unclear behavior.

We split this skill into the rules it gives an agent

24 rules. About 0 are common, 23 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈23

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If you are reading this, the user is working with code that lacks trustworthy tests or has unclear behavior.

    line 18
  2. Distinctive:

    If current behavior is uncertain, characterize it — write tests that capture what the code does today, even if the behavior is ugly.

    line 40
  3. Distinctive:

    Write characterization tests when you don't know whether current behavior is intentional.

    line 45
  4. Distinctive:

    Test externally visible behavior first — narrow tests around the slice you're modifying.

    line 46
  5. Distinctive:

    If real consumers rely on ugly behavior, capture it.

    line 47
  6. Distinctive:

    Use sensing variables or temporary probes only to confirm a test reaches the intended path.

    line 48
  7. Distinctive:

    Prefer seams that remain useful after the current task over one-off test hooks

    line 55
  8. Distinctive:

    When code is hard to test, look for these dependency types and break them:

    line 59
  9. Distinctive:

    When direct edits are too risky:

    line 69
  10. Distinctive:

    Sprout class: Add a focused new collaborator.

    line 72
  11. Distinctive:

    Wrap method: Add pre/post behavior around a risky method, or observe effects through a wrapper.

    line 73
  12. Distinctive:

    Uncertain behavior: When behavior is unclear, consumers may rely on ugly behavior, or a branch is hard to prove — add characterization …

    line 83
  13. Distinctive:

    Expensive setup: When tests require too much setup or a class can't be instantiated cheaply, break the first real barrier: constructor …

    line 84
  14. Distinctive:

    Non-determinism: When time, randomness, environment, thread-local state, files, network, or DB writes block repeatable tests — wrap or …

    line 85
  15. Distinctive:

    Large method/class: When a method or class defeats local reasoning, sketch effects, find interception or pinch points, extract pure …

    line 86
  16. Very specific:

    Framework-bound code: When changing database-heavy, UI, framework, or API-boundary code — separate policy from …

    line 87
  17. Distinctive:

    Temporary seam: When a seam is magical, public-for-test, subclass-only, or link/preprocessor-based — add a cleanup obligation.

    line 88
  18. Distinctive:

    Repeated edits: When repeated edits cluster across several places, remove duplication incrementally under tests instead of launching a …

    line 89
  19. Distinctive:

    Rewrite temptation: When a rewrite feels tempting, choose the smallest sprout, wrap, seam, or characterization step that makes today's …

    line 90
  20. Distinctive:

    Rewrite as first move: Do not replace a subsystem before understanding current behavior.

    line 94
  21. Distinctive:

    No-safety change: Do not change legacy code with no tests or observation strategy.

    line 95
  22. Distinctive:

    Hidden dependency expansion: Do not add more globals, statics, or ambient context to already hard-to-test code.

    line 96
  23. Distinctive:

    Cosmetic refactoring only: Do not rename and format while leaving the real dependency knots intact.

    line 97
  24. Distinctive:

    Before finalizing any legacy change:

    line 101

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Working Effectively with Legacy Code

When This Skill Loads

If you are reading this, the user is working with code that lacks trustworthy tests or has unclear behavior. Stop and follow this workflow. Do not start with a rewrite, a broad cleanup, or speculative refactoring.

Core Principle

Gain control before improving design.

The legacy loop:

  1. Identify the change point
  2. Characterize current behavior (write tests that capture what it does today)
  3. Find or create the smallest useful seam
  4. Break the dependency that blocks testing
  5. Change the requested behavior
  6. Refactor locally — leave the area more testable

Primary Directive

If a part of the code lacks trustworthy tests, treat it as legacy code.

Before editing, state:

  • The requested behavior change (what should be different)
  • The current behavior that must remain (what must not break)

If current behavior is uncertain, characterize it — write tests that capture what the code does today, even if the behavior is ugly. Do not silently "fix" behavior you don't understand.

Decision Rules

Characterization

  • Write characterization tests when you don't know whether current behavior is intentional.
  • Test externally visible behavior first — narrow tests around the slice you're modifying.
  • If real consumers rely on ugly behavior, capture it. Mark it for clarification, don't silently fix it.
  • Use sensing variables or temporary probes only to confirm a test reaches the intended path; remove them after use.

Seams

A seam is a place where you can alter behavior without editing in that place. Use the smallest seam that unlocks the change:

  • Constructor injection, parameter injection, extracted method, wrapper around static call, adapter around a framework object, factory indirection, module boundary
  • Subclass seam only when language constraints leave no better option
  • Link, import, or preprocessing seams only when ordinary object seams are impractical
  • Prefer seams that remain useful after the current task over one-off test hooks
  • Decide whether each seam is for sensing (observing behavior), separation (substituting a dependency), or both

Dependency Breaking

When code is hard to test, look for these dependency types and break them:

Dependency TypeExamplesBreaking Move
Hidden inputsTime, randomness, env vars, thread-local state, globals, singletonsInject clock, random generator, config
Hard outputsFile writes, network calls, DB writes, process exits, messagesWrap behind an interface, inject the wrapper
Construction problemsConstructors doing real work, hidden new, buried factory callsSplit construction from use, parameterize constructor
Static/global reach-throughDirect static calls, global config accessEncapsulate behind a wrapper, inject the wrapper

Sprout and Wrap Techniques

When direct edits are too risky:

  • Sprout method: Extract new behavior into a new method. Keep old code mostly untouched. Route to the new method from a small insertion point.
  • Sprout class: Add a focused new collaborator. Delegate from the legacy class. Move behavior over incrementally.
  • Wrap method: Add pre/post behavior around a risky method, or observe effects through a wrapper.
  • Wrap class: Mediate through a new abstraction when the original class is too hard to test directly.

Keep Changes Separate

  • Behavior changes and structural refactorings go in separate commits.
  • Characterization tests (capturing old behavior) and new behavior tests stay distinct.
  • Exploratory restructuring for understanding: do not check it in.

Trigger Rules

  • Uncertain behavior. When behavior is unclear, consumers may rely on ugly behavior, or a branch is hard to prove — add characterization before changing semantics.
  • Expensive setup. When tests require too much setup or a class can't be instantiated cheaply, break the first real barrier: constructor work, hidden allocation, factory call, global state, static construction.
  • Non-determinism. When time, randomness, environment, thread-local state, files, network, or DB writes block repeatable tests — wrap or inject that boundary.
  • Large method/class. When a method or class defeats local reasoning, sketch effects, find interception or pinch points, extract pure computation first.
  • Framework-bound code. When changing database-heavy, UI, framework, or API-boundary code — separate policy from query/mapping/persistence/handlers.
  • Temporary seam. When a seam is magical, public-for-test, subclass-only, or link/preprocessor-based — add a cleanup obligation. Remove it once safer structure exists.
  • Repeated edits. When repeated edits cluster across several places, remove duplication incrementally under tests instead of launching a broad redesign.
  • Rewrite temptation. When a rewrite feels tempting, choose the smallest sprout, wrap, seam, or characterization step that makes today's requested change safer.

Forbidden Patterns

  • Rewrite as first move. Do not replace a subsystem before understanding current behavior.
  • No-safety change. Do not change legacy code with no tests or observation strategy.
  • Hidden dependency expansion. Do not add more globals, statics, or ambient context to already hard-to-test code.
  • Cosmetic refactoring only. Do not rename and format while leaving the real dependency knots intact.

Final Checklist

Before finalizing any legacy change:

  • Untested area treated as legacy risk?
  • Behavior delta and behavior-to-preserve stated?
  • Uncertain current behavior characterized?
  • Tests close enough and fast enough to diagnose the change?
  • Smallest useful seam chosen (sensing vs separation clear)?
  • Blocking dependency reduced without expanding hidden dependencies?
  • Behavior change, refactoring, and cleanup kept separate?
  • Temporary seam has a cleanup path?
  • Touched area is more understandable, testable, or changeable than before?

Reference

FileUse When
Dependency BreakingChoosing which dependency-breaking technique applies to a specific barrier
Seam TechniquesDeciding between sprout method, sprout class, wrap method, wrap class, extract-and-override
Legacy WorkflowFull step-by-step walkthrough of the legacy change loop

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.