# Run TS Cleanup

> Use if cleaning up a TypeScript codebase — dead code, unused deps, AI slop, weak types.

- Skill: `yigitkonur/run-ts-cleanup` (Agent Skill, multi-file: 17 files)
- Install (CLI): `npx skillmds@latest add yigitkonur/run-ts-cleanup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yigitkonur/run-ts-cleanup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: yigitkonur (https://skillmd.com/u/yigitkonur)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yigitkonur/run-ts-cleanup

---


# Run TS Cleanup

A TypeScript codebase accumulates. Features get deprecated but their files stay. A refactor lands half-finished and both abstractions survive. An LLM writes `cn` for the fourth time in a fourth directory. Dependencies outlive the code that imported them. Types get exported "just in case" and never consumed. This skill sweeps all of it into a clean end state:

- Every **dead symbol, file, export, type, and dependency** is found by a real engine, not by guesswork — and each finding is **triaged before deletion**, never deleted on the engine's word alone.
- Every deletion lands in a **reversible wave** behind a passing verification gate, so any single step reverts with one `git revert`.
- The **root cause** is addressed, not just the symptom: duplicate helpers get consolidated, circular barrels get untangled, single-use wrappers get inlined — so the same bloat does not regrow.
- The codebase ends **more navigable than it started**, for the next agent and the next human, without a single behavioural change.

This is a *finishing* tool. Run it on a green main branch, not mid-feature.

## When To Use

Trigger on phrases and repo states like:

- *"clean up this TypeScript project"*, *"my codebase is a mess"*, *"remove the dead code"*
- *"find unused dependencies"*, *"prune unused exports"*, *"delete orphaned files"*
- *"there's too much AI slop in here"*, *"tighten up the types"*, *"untangle these imports"*
- *"set up knip"*, *"run knip"*, *"why is this bundle so big"*
- Repo state: after a feature deprecation, a framework migration, or a long LLM-driven build-out.

Do **NOT** use this skill for:

- *Pure formatting or style-only changes* — run the project's own formatter; this skill deletes code.
- *Branch, worktree, and repo-artifact cleanup* — use `run-repo-cleanup`.
- *Reviewing a diff or PR* — use `run-review`.
- *A dirty working tree* — commit or stash first; Phase 0 refuses to start otherwise.
- *A red baseline* — fix the failing build or tests first. Cleanup on a broken repo cannot tell your breakage from its own.

## Modes

Pick the mode explicitly at the start, from the user's ask. Announce which one you picked. Escalating mid-run is fine; silently doing more than was asked is not.

| Mode | Runs | Fires on |
|---|---|---|
| **Sweep** | Phase 0-1, then lint autofix and Wave 1 only | *"tidy this up quickly"*, *"drop the unused deps"* |
| **Standard** | Phases 0-3, then Waves 1-5 | *"clean up the dead code"* — **the default** |
| **Deep** | Standard, plus Phase 4 root-cause work, type hardening, and structural refactor | *"the codebase is a mess"*, *"make this maintainable"* |

## The Engines

No single tool finds everything. Each engine sees one scope and is blind to the others.

| Engine | Finds | Scope |
|---|---|---|
| **Knip** | Unused files, exports, types, dependencies | Module graph |
| **Biome / Oxlint / ESLint / Ultracite** | Unused locals, dead imports, correctness lint | File AST |
| **`tsc`** | Type errors, declaration-emit breakage | Program |
| **`type-coverage`** | `any`-creep, as a number | Program |
| **`madge`** | Circular dependencies | Import graph |
| **anti-slop (Oxlint plugin)** | LLM-generated antipatterns | File AST |

Knip and the linters are complements, not alternatives: Knip finds a file nothing imports, a linter finds an import nothing uses. Read [references/engines/engine-matrix.md](references/engines/engine-matrix.md) for the full matrix and the auto-detection protocol.

## Pinned Defaults

Decided once; override per-project only if the repo says otherwise (`AGENTS.md` / `CLAUDE.md` / `CONTRIBUTING.md` win over this skill).

| Key | Default | Why |
|---|---|---|
| **the gate** | `tsc --noEmit` → `test` → `build`, resolved for the detected package manager | The single verification barrier. Defined once in [references/remediation/waves.md](references/remediation/waves.md); every "Run the gate" in this skill means exactly that. |
| **branch** | `chore/ts-cleanup` | Never work on `main`. |
| **config file** | `knip.jsonc` (fallback `knip.json`) | JSONC carries comments explaining why each entry exists. |
| **package manager** | Detected from `packageManager` field, else lockfile | Never hardcode `pnpm`. See waves.md §1.2. |
| **wave order** | Deps → Files → Barrels → Exports → Types | Reverse order leaves dangling imports and ghost references. |
| **commit unit** | One commit per wave | Each wave reverts independently. |
| **suppression** | Targeted keys only | `ignoreDependencies`, `ignoreBinaries`, `ignoreExportsUsedInFile`, explicit `entry`. |

## Non-Negotiable Safety Rails

1. **Start green.** Verify `git status --porcelain` is empty, the typecheck passes, and the test suite passes before touching anything. A cleanup that begins on a red baseline cannot distinguish its own damage from pre-existing damage.
2. **Suppress false positives with targeted keys.** Reach for `ignoreDependencies`, `ignoreBinaries`, `ignoreExportsUsedInFile`, or an explicit `entry` — each one silences exactly the finding it names. A broad top-level `ignore` severs graph edges instead, so files the ignored code imports start reporting as dead and get deleted.
3. **Triage every finding before deleting it.** Apply the Three-Question Verification Test to each one. An engine reports what it cannot see a reference to; framework entry points, dynamic imports, and published API surfaces are all invisible to it.
4. **Delete in wave order.** Dependencies, then files, then barrels, then exports, then types. Each wave's output is the next wave's input.
5. **Run the gate after every wave, before every commit.** A wave that fails the gate is reverted or fixed, never committed.
6. **Prove declaration-emit safety before un-exporting.** `tsc --noEmit` builds the implementation graph and never the declaration graph, so it passes on code that `tsc -b` will reject. See [references/types/declaration-emit.md](references/types/declaration-emit.md).
7. **Fix the cause, then the symptom.** Consolidate the four copies of `cn` before deleting three of them; migrate the call sites before pruning the abstraction.

## The Six Phases

```
        dead code · unused deps · AI slop · weak types · tangled imports
                              │
   Phase 0 — Baseline: clean tree, green typecheck, green tests, branch cut
                              │
   Phase 1 — Engine setup: detect the stack, configure each engine, resolve the manager
                              │
   Phase 2 — Survey: run every engine, batch findings, score risk
                              │
   Phase 3 — Triage: every finding gets a verdict — dead, false-positive, or deferred
                              │
   Phase 4 — Root cause: consolidate duplicates, untangle cycles, inline wrappers  [Deep]
                              │
   Phase 5 — Waves: delete in order, gate after each, one commit per wave
                              │
     zero dead code · green gate · one revertable commit per wave · a report you trust
```

Each phase gates the next. If you are tempted to skip one, re-survey instead.

---

## Phase 0 — Baseline

**Think first:** *"Is this repo green right now, and can I prove it?"*

1. Refuse to proceed on a dirty tree:
   ```bash
   test -z "$(git status --porcelain)" || { echo "Dirty tree — commit or stash first."; exit 1; }
   ```
2. Cut the working branch: `git checkout -b chore/ts-cleanup`
3. Resolve the package manager and write the gate script once — [references/remediation/waves.md](references/remediation/waves.md) §1.2-1.4 gives the detection snippet.
4. **Run the gate.** Record that it passed. This is the baseline every later gate is compared against.
5. Capture the starting metrics for the final report: finding counts per engine, `type-coverage` percentage, cycle count.

**Gate → Phase 1 when:** the tree is clean, the branch exists, the gate passes, and baseline metrics are recorded.

---

## Phase 1 — Engine Setup

**Think first:** *"Which engines does this repo already have, and which does it need?"*

1. Detect what is installed — Biome, Oxlint, ESLint, Prettier, Ultracite, Knip, madge, type-coverage:
   ```bash
   python3 scripts/audit-ts-health.py --target .
   ```
   With no `--check-*` flag it runs all four audits. Passing any `--check-*` flag narrows it to just those.
2. Generate or review the Knip config:
   ```bash
   python3 scripts/init-knip-config.py --dry-run     # inspect first
   python3 scripts/init-knip-config.py --format jsonc
   ```
   It detects frameworks and monorepo layout, and stamps a `$schema` matching the installed Knip major version.
3. Resolve configuration hints before trusting any finding. Knip reports config hints ahead of issues; a hint left unresolved produces a cascade of false findings downstream. Tune entry points until the hints are gone — [references/engines/knip-configuration.md](references/engines/knip-configuration.md) has the plugin catalog, monorepo patterns, and compiler settings.
4. Confirm zero broad top-level `ignore` patterns in the config.

**Gate → Phase 2 when:** every engine the mode needs is runnable, config hints are resolved, and no broad ignores remain.

---

## Phase 2 — Survey

**Think first:** *"What does every engine see, and how much of it is worth acting on?"*

1. Run the dead-code engine and batch its output:
   ```bash
   python3 scripts/batch-findings.py --run --output markdown --out-file cleanup-plan.md
   ```
   Batches 1-6 come from the dead-code engine, each with a risk score and a gate rendered for the detected manager.
2. Run the remaining engines the mode calls for — lint, `type-coverage`, `madge`. Their findings form batches 7-9. Commands in [references/engines/lint-engines.md](references/engines/lint-engines.md) and [references/engines/analysis-engines.md](references/engines/analysis-engines.md).
3. Read the batch definitions and risk model in [references/detection/finding-classification.md](references/detection/finding-classification.md) — nine batches, each naming its producing engine and the wave it feeds. Several collapse: batches 4 and 5 both feed Wave 4, 3 and 9 feed Wave 3, 6 and 8 feed Wave 5.

**Gate → Phase 3 when:** `cleanup-plan.md` exists, every finding sits in a batch, and every batch carries a risk score.

---

## Phase 3 — Triage

**Think first:** *"Which of these findings would break production if I deleted it?"*

Apply the **Three-Question Verification Test** to every finding:

1. **Framework lifecycle?** Does a convention invoke this without importing it — a Next.js `page.tsx` / `route.ts`, a Remix `loader`, an Astro content config, a Playwright fixture?
2. **Dynamic invocation?** Is it reached through a template-literal `import()`, a string-keyed registry, or runtime reflection?
3. **Public contract?** Is it exported to consumers outside this repo's static graph, via `exports` or `typesVersions`?

Verify with the codebase, not from memory:

```bash
rg -g '!node_modules' -g '!dist' 'symbolOrPackageName'
```

Codify each confirmed false positive back into config as a targeted key — never as a broad ignore. Framework entry catalogs, dynamic-import patterns, and lint/type false positives are in [references/detection/false-positive-triage.md](references/detection/false-positive-triage.md).

**Gate → Phase 4 when:** every finding in `cleanup-plan.md` carries an explicit `verdict` of `dead`, `false-positive`, or `deferred`. Zero blanks. Count them and state the number.

---

## Phase 4 — Root Cause

**Think first:** *"Why did this bloat appear, and what stops it coming back?"*

Deep mode only. Sweep and Standard skip to Phase 5.

Deleting the symptom leaves the generator running. Work out which origin produced each cluster, then fix that:

| What you found | Where to go |
|---|---|
| Four copies of `cn`, duplicate `formatDate`, useless try/catch, phantom null checks, boolean theater | [references/detection/code-slop-catalog.md](references/detection/code-slop-catalog.md) — origins and per-pattern remediation |
| Circular imports, bloated barrels, single-use wrappers, scattered helpers | [references/remediation/structural-refactor.md](references/remediation/structural-refactor.md) — plan-first structural work |
| Brain methods, deep nesting, long parameter lists, speculative generality | [references/remediation/complexity-thresholds.md](references/remediation/complexity-thresholds.md) — numeric thresholds to prioritise by |
| `any` sprawl, missing strict flags, weak inference | [references/types/strict-migration.md](references/types/strict-migration.md) — the ratchet |

Consolidation is a code change like any other: run the gate and commit it before starting Wave 1.

**Gate → Phase 5 when:** duplicates are consolidated behind one canonical implementation, call sites are migrated, cycles are broken, and the gate passes.

---

## Phase 5 — Waves

**Think first:** *"What is the smallest deletion I can make, prove, and commit right now?"*

Five waves, in order. After each: **run the gate**, then commit. A wave that fails the gate gets fixed or reverted — never committed. Full protocol, per-wave scope, and rollback in [references/remediation/waves.md](references/remediation/waves.md).

| Wave | Deletes | Additional check | Commit |
|---|---|---|---|
| **1** | Unused dependencies, then refresh the lockfile | — | `chore(deps): prune unused dependencies` |
| **2** | Orphaned and unreferenced files (`git rm`) | — | `chore: remove unreferenced files` |
| **3** | Dead barrel re-exports; repoint consumers at source modules | — | `refactor: prune dead barrel re-exports` |
| **4** | Test-only leaks and in-file-only exports (strip `export`) | Declaration emit, then **lint autofix** | `refactor: internalize private exports` |
| **5** | Unused types, interfaces, enum members | Declaration emit | `refactor: prune dead types` |

**Wave 4 needs the linter bridge.** Stripping an `export` leaves the importing files holding specifiers that now reference nothing. Run the detected engine's autofix immediately after — recipes in [references/engines/lint-engines.md](references/engines/lint-engines.md), timing rationale in waves.md §7. Verify declaration emit before stripping anything reachable from a public signature: [references/types/declaration-emit.md](references/types/declaration-emit.md).

### Post-flight

1. Re-run every engine. Confirm zero remaining findings, or an explicit deferred list.
2. Diff the metrics against the Phase 0 baseline: findings removed, `type-coverage` delta, cycle count delta, dependency count delta.
3. Remove scratch artifacts: `cleanup-plan.md`, `.cleanup-gate.sh`, any baseline JSON.
4. Report what was deleted, what was deferred and why, and the metric deltas.

**Gate → done when:** every wave is committed, the gate passes on the final commit, engines report clean, and the report is delivered.

---

## Common Mistakes

| Mistake | Consequence | Instead |
|---|---|---|
| Broad top-level `"ignore": ["src/legacy/**"]` | Knip stops traversing those files, so their imports report as dead and get deleted | Use `ignoreDependencies` / `ignoreBinaries` / `ignoreExportsUsedInFile` / explicit `entry` |
| Acting on findings before resolving config hints | Every downstream finding is suspect; you triage noise | Clear hints in Phase 1, then survey |
| One big cleanup commit | No bisect, no partial revert, unreviewable | One commit per wave |
| Deleting exports before pruning dependencies | Dangling imports and confusing compiler cascades | Wave order: deps → files → barrels → exports → types |
| Trusting `tsc --noEmit` before un-exporting | It never builds the declaration graph; `tsc -b` then fails on TS4023 | Verify declaration emit explicitly |
| Hardcoding `pnpm` in the gate | The gate silently no-ops on npm/yarn/bun repos | Resolve the manager from `packageManager` or the lockfile |
| Deleting duplicate helpers without migrating callers | Each copy had drifted; consumers depended on the differences | Consolidate, migrate, gate, *then* delete |
| Skipping Phase 0 because the repo "looks fine" | Pre-existing failures get attributed to the cleanup | Prove green first |

## Scripts

Python 3 stdlib only — no dependencies, no network beyond the engines they invoke.

| Script | Phase | Purpose | Mutates? |
|---|---|---|---|
| `scripts/audit-ts-health.py` | 1, 4 | Detects linters and formatters, audits `tsconfig.json` flags, finds import cycles, flags slop markers. | No |
| `scripts/init-knip-config.py` | 1 | Scans frameworks and monorepo layout; emits `knip.jsonc`/`knip.json` with a version-matched `$schema`. | Yes — `--dry-run` to preview, `--force` to overwrite |
| `scripts/batch-findings.py` | 2 | Runs or parses the dead-code engine; batches findings, scores risk, renders gates for the detected manager. | Only with `--out-file` |

Run any script with `--help` for its full flag surface.

## References

- [references/engines/engine-matrix.md](references/engines/engine-matrix.md) — which engine finds which class of problem, at what scope and speed; the auto-detection protocol.
- [references/engines/knip-configuration.md](references/engines/knip-configuration.md) — Knip config formats, schema, monorepo workspace isolation, compilers, the plugin catalog, and targeted-vs-broad ignores.
- [references/engines/lint-engines.md](references/engines/lint-engines.md) — Biome, Oxlint, ESLint, and Ultracite recipes; safe formatting rules versus mutations to avoid mid-remediation.
- [references/engines/analysis-engines.md](references/engines/analysis-engines.md) — `type-coverage`, `madge`, and the anti-slop Oxlint plugin: install, invoke, read the output.
- [references/detection/finding-classification.md](references/detection/finding-classification.md) — the six batches, which engine produces each, the wave each feeds, and the risk model.
- [references/detection/false-positive-triage.md](references/detection/false-positive-triage.md) — the Three-Question Test, framework entry catalogs, dynamic imports, public contracts, and lint/type false positives.
- [references/detection/code-slop-catalog.md](references/detection/code-slop-catalog.md) — architectural origins of bloat and the slop patterns they produce, with detection commands and before/after remediation.
- [references/remediation/waves.md](references/remediation/waves.md) — **the gate**, package-manager resolution, all five waves, the linter bridge, rollback, and the post-flight metrics gate.
- [references/remediation/structural-refactor.md](references/remediation/structural-refactor.md) — plan-first structural work: inlining wrappers, breaking cycles, restructuring barrels, co-locating helpers.
- [references/remediation/complexity-thresholds.md](references/remediation/complexity-thresholds.md) — cyclomatic and cognitive complexity thresholds and the code-smell index, for prioritising what to refactor first.
- [references/types/strict-migration.md](references/types/strict-migration.md) — eliminating `any`-creep, the type-coverage ratchet, incremental strict-mode migration, type tidying.
- [references/types/declaration-emit.md](references/types/declaration-emit.md) — why un-exporting breaks `tsc -b`, the TS4023/4081/4082/4060/2742/2883 matrix, and the pre-un-export checklist.

## Bottom Line

Prove green → configure the engines → survey every scope → give every finding a verdict → fix the cause → delete in five gated waves → report the deltas.

