What does untangle do?
A tangled codebase is a pile of decisions nobody made. The AI wrote whatever the next prompt asked for, never refused, and never proposed a rethink, so the repo now holds three ways to do each thing and no record of why. Untangling means making each of those decisions once, writing it down, and making the code and its checks agree with it.
You do the mapping, the evidence and the edits. The human makes the decisions.
Target state
The work is done when all five hold:
- One way to do each thing: auth, data access, errors, config, logging.
- Tests fail when behaviour is wrong.
- Every surviving piece has a reason someone can state.
- Decisions are written down where the next person or agent will read them.
- Automated checks stop the tangle growing back.
If the codebase already meets these, say so and stop. Inventing work is a failed audit.
Rules for every phase
- The ledger is the state.
UNTANGLE.mdat the repo root records coverage, findings, decisions and progress. Read it first and update it before your turn ends. A long cleanup outlives your context window; the ledger is what carries over. If it exists, resume from it instead of auditing again. - Evidence or it is not a finding. Each finding carries
file:lineand the command or observation that shows it. "Smells messy" is not a finding. - You propose, the human decides. Nothing is deleted, merged or declared the winner without an approved ledger row. Recommend a default for every decision so approval is cheap.
- Structure and behaviour change in separate commits. A refactor that also fixes a bug cannot be verified as either.
- Report only what you ran. Record commands with their real results. Record what you read closely, what you only mapped, and what you never opened.
- Subtract. No rewrite, no new dependency, no new abstraction layer, no compatibility shim. The line count should fall.
- Secrets. On finding a live credential, stop and tell the human to rotate it. Never print its value. Never rewrite history yourself.
Phases
Audit → Triage → Refactor → Sustain, in that order. With no ledger, start at Audit. With a ledger, continue from the phase it records. An argument naming a phase or a path narrows the work. It never skips Audit, because Refactor acts only on approved findings.
1. Audit: read-only
Changes nothing except UNTANGLE.md. Start it from the template in templates.md.
- Baseline. Find the real install, build, test, lint and typecheck commands and run them. Record the results verbatim. A repo that will not build from its README is finding one.
- Map before reading. File listing, sizes, entry points, import direction, churn from git history. Then choose where to read closely, riskiest first: auth, money, data writes, infra. Record coverage per path.
- Hunt using
catalog.md. Run analysis tools ephemerally (npx,uvx,go run) so the project's dependencies stay untouched. - Test the tests. Pick three business rules. For each one, change the rule so it returns a wrong answer without raising, run the suite, restore the file with
git restore, and confirmgit statusis clean. Record whether any test noticed. - Score the scorecard in
templates.md. Each check is pass, fail, or "not verifiable from the repo" with the question the human must answer. - Report and stop. Top findings by risk, what is fine, what was not covered.
2. Triage: decide
- Before listing fixes one by one, look for the single decision that dissolves several findings. Ask what this code would look like if its requirements had been known from the start.
- For every concern with competing implementations, recommend a winner and give the reason: most call sites, best tested, matches the framework's convention.
- Mark each finding keep, fix or remove. Rank by risk: chance of harm × how much depends on it.
- Order the slices. Safety net first, then removals (cheap, and they shrink everything after them), then consolidation, then restructuring.
- Present one table, get approval in one pass, and stop until you have it. Record rejections as
wontfixwith the reason.
3. Refactor: one slice at a time
Work on a branch named untangle/<topic>. Never push. For each approved slice:
- Net. Tests cover the behaviour this slice touches, and you have watched them fail under a deliberate break. If they do not, write characterization tests first: pin what the code does today, bugs included. Log each bug as a new finding instead of fixing it silently.
- Change one concern.
- Verify with the full baseline commands. On red, fix the change or revert the slice.
- Commit with the finding IDs, update the ledger, and continue to the next slice without asking.
Stop and ask when a slice needs a decision the ledger does not hold.
"Dead" means proven unreachable: a static tool's verdict plus a search for dynamic reach through string lookups, reflection, routes, DI names, config, cron, templates, public API and callers outside the repo. Anything unproven is suspect and goes to the human.
4. Sustain: make it stick
- For each decision, ask whether a machine can enforce it. If it can, add the lint rule, import boundary, CI step or test. Prose rules are for what machines cannot check.
- Write
AGENTS.mdfrom the decisions: commands, the one way per concern with a pointer to an example file, and where new code goes. Keep it short enough to be read every session. IfCLAUDE.md,.cursorrulesor similar files exist, make one the source and have the rest point to it. - Write an ADR for each decision that picked a winner.
- Make the README's clean-checkout setup true, and commit an env template.
- CI runs the baseline commands and fails on red. Branch protection and alert routing live outside the repo, so list them for the human.
- Re-score, record before and after, and leave unfinished items listed in the ledger.
When you are tempted
| Thought | Do instead |
|---|---|
| "I'll fix this while I'm here" | Add a finding. Unapproved changes are how the tangle was made. |
| "The test is outdated, I'll adjust the assertion" | A red test after a structural change means the change altered behaviour. Fix the change or revert it. |
| "Nothing references this, it's dead" | Prove it against dynamic reach, or mark it suspect. |
| "A wrapper keeps the old callers working" | Update the callers. A shim is surplus with a new name. |
| "This should just be rewritten" | Slices. A rewrite discards every behaviour the tests do not cover. |
| "I've covered the codebase" | Coverage is what the ledger says you read. |
| "Context is getting long, I'll wrap up fast" | Write the ledger, say what remains, stop. |
| "The suite passes, so it works" | A pass proves only what audit step 4 showed the tests can catch. |