agentleFS
H2OArcticH2OArctic/claude-skill-code-tidy/skill/SKILL.md

code-tidy

Skill2 starsChanged 3 months agoMITView on GitHub

Tidy up a TypeScript/Bun codebase without changing behaviour — sequential awaits and N+1 queries, duplication and near-identical enums, hardcoded values, types and constants mixed into implementation files, oversized or multi-component files, logic living inside components, components worth moving into a shared package, custom CSS and hardcoded style values in a Tailwind project, inconsistent idioms, import cycles, leftover debug code. Use when asked to clean up, tidy, declutter, deduplicate, split a large file or component, extract shared components, make an area consistent, or prepare it before building on it.

4 other projects use a version of this.

Adopted by

Most recently taken up by

  • Other versions4 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 4

    1. lltx/skills★ 14same name
    2. DoKu67/claude-code-skils★ 4same name
    3. lousy-agents/skills★ 1same name
    4. rupertsworld/skills★ 1same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

The file

We split this skill into the rules it gives an agent

2 rules. 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 1

    Distinctive

    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. Very specific:

    Read the root package.json and use the project's own script names.

    line 56
  2. Distinctive:

    Do not introduce tooling: No new linter, no new config, no new rule in an existing config, no formatter run over files the task never …

    line 58

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “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

Code tidying (TypeScript / Bun)

Runtime and package manager: Bun only. Never npm, npx, yarn, pnpm, or node.

This skill removes what is not code any more and evens out what is written five different ways. It does not improve behaviour: bug fixes, typing, performance work and API design are different tasks and leave this one with a line in the report.

Talk to the user in the language they use.

Invariants

1. The diff does not change behaviour. The proof is not "the tests passed" — it is a baseline recorded before the first edit and compared after. Without that baseline a green run proves nothing, because nobody knows what was green to begin with.

2. The project's own rules outrank this skill. CLAUDE.md, AGENTS.md, .agents/rules/, docs/conventions.md, CONTRIBUTING.md — read them in phase 0 and follow them. Where they say something different from the defaults below, they win, and the report says so. Where they are silent, the code's own dominant pattern wins. This skill's opinions come last.

3. Nothing is written until the plan is approved (phases 0–2 are read-only). Linters run in check mode only; --fix, --write and --unsafe wait for phase 4.

4. Formatting and meaning never share a commit. A reformatted file hides its one real change from review.

5. The scope is what the user asked for. "Tidy this module" is not permission to tidy the repository.

6. Никаких выводов по обрезанному списку. Текстовый отчёт печатает верхушку каждого раздела и внизу перечисляет, что обрезано. Разобрать восемь случаев из тридцати четырёх и сказать «в основном это одно и то же» — не разбор, а угадывание: остальные двадцать шесть никто не смотрел. Поэтому план строится по --json, где списки полные, а в отчёте у каждого класса стоит разобрано N из M. Пока M больше нуля и не разобрано — работа не закончена, и слово «готово» не произносится.

Phase 0. Scope, rules, preflight

git status --porcelain          # рабочее дерево должно быть чистым
git branch --show-current
bun --version
  • Read the project's rules first. The scan lists the rule files it found, with line counts. Read them before proposing anything: a "violation" that the project deliberately allows is not a finding, and a rule the project states explicitly ("SQL only in repositories", "no string literals", "env only through the config module") turns half the report from opinion into fact.
  • Decide the scope, and say it. Preference order: the current diff → a named directory → the whole repository, and the last only when asked for exactly that. --scope diff|branch|<путь>.
  • Dirty tree → read the diff before asking anything (git diff --stat). Someone's work in progress inside the target area moves those files to group C.
  • Read the root package.json and use the project's own script names. Note that a project can have more than one test command (unit and integration are often split, and the second one is not run by the first).
  • Do not introduce tooling. No new linter, no new config, no new rule in an existing config, no formatter run over files the task never mentioned.

Phase 1. Baseline (read-only)

bun run typecheck        # или bunx tsc --noEmit -p tsconfig.json
bun run build            # если есть
bun test                 # и второй тестовый скрипт, если он есть
  • A command that exits 0 without doing anything is not a passing check. 0 tests found, no tasks were executed, a task runner that skipped every workspace — that is a missing check.
  • A red baseline is not a blocker, it is a record. List what already fails, so that afterwards nobody blames the cleanup for it.
  • No tests at all → say so plainly and narrow the plan to group A.

Phase 2. Inventory (read-only)

bun ~/.claude/skills/code-tidy/scripts/tidy-scan.ts                 # обзор: верхушка каждого раздела
bun ~/.claude/skills/code-tidy/scripts/tidy-scan.ts --json          # полные списки — план строится по ним
bun ~/.claude/skills/code-tidy/scripts/decisions.ts list            # что уже решили не трогать

Текстовый прогон — для человека и для первого взгляда. Разбирать находки по нему нельзя: он показывает 6–10 строк на список, а внизу честно перечисляет, чего не показал. Как только раздел попал в этот перечень, дальше работать только с --json.

Then whatever the project already has, in check mode only: bunx tsc --noEmit, bunx biome check ., bunx eslint ., bunx knip.

Раздел отчётаЧто с этим делать
1. Ломает прямо сейчасit.only, debugger, .forEach(async), пустой catch, sql.unsafe, секрет в логе — первым делом и отдельно
2. Мусорудаляется без обсуждения (группа A)
3. Асинхронностьawait в цикле и цепочки await — группа B: правка меняет семантику
4. Повторыблоки, похожие перечисления, зашитые значения — разговор, а не команда «вынести»
5. Крупные единицысортировка по вниманию; размер сам по себе не дефект
6. Раскладка и именованиесверено с правилами этого проекта; «против правила проекта» — сильный аргумент
7. Компоненты и стилиодин файл — один компонент, логика наружу, общее в пакет, только Tailwind
8. Единообразиепоказан раскол и меньшинство; решение — привести к большинству или узаконить оба
9. Связностьциклы и barrel-файлы — структурная правка
10. Зона рискачто не трогать: чужие правки, горячие файлы, генерация
11. Журналотложенное; вернувшиеся вопросы — с объяснением, что изменилось

Раздел 1 — это не уборка, а поломки. Часть из них снимается удалением (.only, debugger), но компонент внутри компонента, хук под условием, список без key, пустой catch, sql.unsafe и секрет в логе чинятся настоящей правкой кода. Показать их первыми и спросить, чинить сейчас или отдельной задачей. Молча чинить нельзя: это меняет поведение — пусть и в лучшую сторону.

Заголовок отчёта отдельно показывает принятое в проекте: ролевые суффиксы (*.const.ts, *.port.ts) и каталоги (types/, consts/). Это не рекомендация скилла — это то, что уже господствует в коде.

Мёртвый код — отдельным заходом

Его нет в обычном прогоне: --dead. Вердикт по мёртвому коду живёт недолго и требует ручной проверки чаще, чем всё остальное, — поэтому он не мешается в отчёте, пока его не попросили.

Когда всё-таки просят: скилл считает достижимость от точек входа, а не количество ссылок, поэтому barrel-файл не оживляет мёртвое имя и не хоронит живое. Но статический анализ не видит:

  • потребителей вне репозитория — опубликованный пакет, соседний репозиторий, exports в package.json;
  • связывание по строке — DI-контейнеры, таблицы маршрутов, метаданные декораторов, реестры моделей;
  • ссылки вне кода — имя в JSON/YAML, шаблоне, миграции, CI-конфиге, Dockerfile, preload;
  • использование только в тестах — это не «мёртв», это вопрос: у кода нет пользователя или тест проверяет деталь реализации.

Зоны, где до кода дотягиваются мимо графа (import() с переменной, import.meta.glob, ns[key]), скилл показывает отдельным списком «вердикта нет». Это кандидаты на чтение, а не на удаление.

Phase 3. The plan, by what may be done to it

Считать, а не оценивать

У каждого класса находок в плане стоит число из --json, а не впечатление: «34 цикла с await», а не «циклы с запросами». Дальше каждый пункт получает решение — правим, откладываем в журнал или оставляем с причиной. Класс закрыт, когда сумма решений равна M. Если на весь класс времени нет, так и написать: «разобрано 8 из 34, остальные 26 — отдельной задачей», и не выдавать восьмёрку за тридцать четыре.

Одинаковые с виду находки одного класса не считаются разобранными по образцу: тридцать четыре цикла — это тридцать четыре разных запроса, из которых часть схлопывается в один IN (…), часть параллелится, а часть обязана остаться последовательной. Общий вывод по четырём из них неверен по построению.

A. Механика — без обсуждения

Behaviour-neutral и откатывается одной командой: мусорные файлы (*.orig, *.rej, foo copy 2.ts), пустые файлы, debugger, отладочный console.log, закомментированный код блоками, неиспользуемые импорты, форматирование по конфигу, который уже лежит в проекте, отдельным шагом.

Исключение внутри группы: it.only меняет объём прогона. Убрать правильно, но это включит тесты, которые никто не гонял. Убрать → прогнать → отдельно разобрать вскрывшееся, и в отчёте сказать, что упавшее не сломано уборкой, а перестало быть выключенным.

B. Требует решения пользователя

Асинхронность. Promise.all — это не оптимизация, а смена семантики, и решает её человек:

  • падает на первой ошибке, остальные промисы продолжают выполняться и их отказы уходят в никуда (allSettled — другая семантика, не замена);
  • порядок побочных эффектов исчезает: если вторая операция рассчитывала на строку, созданную первой, параллельный запуск ломает её молча и не всегда воспроизводимо;
  • нагрузка идёт разом: пул соединений, лимиты внешнего API, блокировки в БД;
  • внутри транзакции параллельные запросы по одному соединению — это гонка, а не ускорение.

await внутри цикла — тот же разговор плюс вопрос о запросе: N однотипных обращений почти всегда складываются в одно (IN (...), батч, join). Ставить вместо цикла неограниченный Promise.all по коллекции неизвестного размера — это замена медленного кода на падающий под нагрузкой.

Повторы. Три копии — повод спросить, а не вынести (см. фазу 4). Похожие перечисления — то же самое: одинаковые наборы вариантов часто оказываются осознанным зеркалом по разные стороны границы, через которую код не переносится (сервер и браузерная сборка, генерируемые типы и ручные словари).

Зашитые значения. Литерал, повторяющийся в нескольких местах, — это незаписанная константа. Куда её класть, определяет проект, а не скилл: смотри раздел «принято в проекте» и правила.

Раскладка и именование. Перенос объявлений в types//consts/, разделение файла, переименование под стиль каталога. Переименование ломает git blame и чужие открытые ветки — оправдано, когда имя врёт.

Единообразие. Меньшинство приводится к большинству — но по умолчанию только в тех файлах, которые и так правятся по задаче. Сплошной проход по репозиторию — отдельная просьба.

Компоненты.

  • Один файл — один компонент. Второй компонент в файле переезжает в свой файл: иначе он не переиспользуется, не тестируется отдельно и тянет за собой соседа при каждом импорте.
  • Логика уезжает из файла представления. Функция, которая не рисует, а считает, форматирует или ходит в сеть, живёт в отдельном файле — там её видно другим экранам и там её можно проверить тестом. Экспортированная логика в файле компонента — тем более: её уже кто-то тянет через компонент. Исключение — хуки: они часть представления и остаются рядом.
  • Общее — в пакет, а не копией. Одноимённые компоненты в разных приложениях и одинаковые блоки, разложенные по разным воркспейсам, переносятся в общий пакет. Копия в каждом приложении расходится молча: правку вносят в одну, а вторая живёт своей жизнью.
  • Только TSX. .jsx и .js в TypeScript-проекте — недоехавшая миграция.
  • Тяжёлый компонент (много пропсов, много useState, глубокий JSX) — это разговор о разделении, а не приговор длине.

Стили.

  • Только Tailwind, своего CSS нет. Единственный оправданный файл стилей — конфигурация темы. CSS-модули, styled-обёртки и инлайновый style={{}} — это второй способ делать то же самое, и расходится он с темой молча.
  • Цвет и размер — токенами, а не значениями. #ff0000 и w-[13px] живут мимо шкалы: тема меняется, а они остаются. Произвольное значение оправдано ровно там, где в шкале нужного шага нет, — и тогда правильнее добавить шаг в тему.
  • Повторяющийся набор классов — это ненаписанный компонент. Одна и та же длинная строка в трёх местах означает, что общий вид уже существует, просто у него нет имени.

Мёртвые экспорты и файлы, barrel-файлы, циклы импортов, публичный API — как раньше, только с флагом --dead для первых.

C. Не трогать

  • сгенерированное и вендоренное (шапка @generated, dist/, __generated__/, *.gen.ts, снапшоты);
  • миграции — их прошлое неизменяемо;
  • файлы с чужими незакоммиченными правками и файлы из открытых веток;
  • горячие файлы (раздел 9), если правка не входила в задачу;
  • намеренные зеркала словарей через границу, которую код не пересекает;
  • всё, что журнал держит в отложенных, пока условие отказа в силе.

D. Это не уборка — только в отчёт

Настоящие баги, any вместо типов, гонки, N+1 как архитектурная проблема, слабый тест. Записать одной строкой с местом и симптомом — и не чинить: багфикс внутри уборочного диффа не пройдёт ревью, и не должен.

Уже отвеченные вопросы остаются отвеченными

bun ~/.claude/skills/code-tidy/scripts/decisions.ts keep dupe:1a2b3c \
  --reason "зеркало словаря для браузерной сборки, объединять нечем" --fingerprint <из --json>

Ключи и отпечатки лежат в каждой находке --json. Отпечаток обязателен везде, где он есть: запись действует, пока код тот же, а переписанный блок возвращает вопрос — прежнее оправдание относилось не к нему. Отложенное не переспрашивается: одна строка «отложено: N» и дальше.

Phase 4. Правки — по одному классу за раз

Порядок: мусор → асинхронность → повторы → раскладка → декомпозиция → единообразие. Каждый класс — отдельный шаг с отдельной проверкой.

Асинхронность

// было: два независимых запроса стоят в очереди друг за другом
const user = await users.byId(id)
const limits = await limits.forPlan(plan)

// стало: одновременно — но только если порядок и семантика ошибки это допускают
const [user, limits] = await Promise.all([users.byId(id), limits.forPlan(plan)])
  • Проверить независимость по коду, а не по виду: второй вызов не должен читать результат первого ни напрямую, ни через общий кэш, ни через строку в БД, созданную первым.
  • Цикл с запросом внутри: сначала попытаться сделать один запрос вместо N. Promise.all по циклу — второй выбор, и тогда с ограничением параллельности.
  • Внутри транзакции ничего не распараллеливать.
  • .forEach(async …) — не оптимизация, а потерянные ошибки: переписывается на for…of с await или на Promise.all(map(...)), и это осознанный выбор между последовательностью и параллельностью.

Повторы

  • Правило трёх: две копии — ждём, три — разговариваем. Объединять стоит, только если копии меняются вместе и означают одно и то же.
  • Случайное сходство: два куска выглядят одинаково, но живут по разным причинам. Объединение свяжет их навсегда, и следующая правка пойдёт через флаг.
  • Признак плохой абстракции: чтобы покрыть различия, в общую функцию добавляется параметр, переключающий поведение. Появился options.mode — остановиться, дубль дешевле.
  • Перечисления: победитель один, остальные становятся импортом. Если словари стоят по разные стороны границы, через которую код не переносится, — оставить оба и записать причину в журнал.
  • Зашитые значения: выносить туда, где проект держит константы, именовать по смыслу домена, а не по значению. Если проект связывает константный объект с одноимённым типом — повторить этот приём. Литерал, встречающийся один раз и читаемый на месте, не трогать.

Компоненты и стили

  • Разделение файла на компоненты — механический перенос: содержимое не меняется, меняются импорты. Переносить по одному и проверять сборку после каждого, а не всё сразу.
  • Вынос логики из компонента: сначала перенести функцию как есть, потом (отдельным шагом) убрать из неё то, что было завязано на замыкание компонента. Смешать это в один шаг — потерять контроль над диффом.
  • Перенос компонента в общий пакет — правка публичного API пакета: сначала согласовать, потом двигать, и одним движением обновить обе стороны.
  • Свой CSS не переписывать «в классы» пачкой: файл за файлом, со сверкой глазами. Замена CSS на утилиты без визуальной проверки — это не уборка, а редизайн вслепую.
  • Произвольное значение (w-[13px]) не заменять ближайшим шагом шкалы молча: сдвиг на пиксель заметен, и решение о нём принимает человек.

Раскладка

  • Переносить объявления по ролям, которые уже приняты в проекте, — и не изобретать новые.
  • Перенос и переименование — разные шаги: вместе они превращают дифф в нечитаемый.
  • После переноса проверить, что импорты обновлены везде, включая тесты и конфиги.

Декомпозиция

  • Резать по швам, которые уже есть: тело цикла, ветка условия, шаг с собственным именем. Куску, которому нельзя дать честное имя, шов не там.
  • handleRequestPart2 — не декомпозиция, а перенос строк.
  • Сигнатура и поведение внешней функции не меняются.
  • Длина сама по себе не дефект: плоский switch на 200 строк читается лучше пяти функций с общим состоянием.

Комментарии

Не трогать комментарий, если код рядом не менялся. Правка комментария даёт дифф с нулевым изменением логики, а CI/CD видит изменённый файл и запускает пересборку и редеплой зависимых приложений впустую. Не переписывать, не переводить, не переформатировать заодно; удалять — только вместе с кодом.

Форматирование

Только конфигом проекта и только отдельным шагом. Крупное переформатирование сносит git blame; если оно нужно, предложить .git-blame-ignore-revs, но записывает туда человек вместе с коммитом.

Phase 5. Проверка

bun run typecheck
bun run build
bun test                      # полный прогон; --changed годится только пока чинишь
git diff --stat               # уборка — это в основном удаления
git diff -w                   # если дифф исчезает, там были только пробелы
git diff                      # прочитать каждый ханк, который не является чистым удалением
  • Диф, который растёт, — уже не уборка: появившиеся строки должны объясняться согласованным пунктом группы B.
  • Правки в асинхронности проверяются не только тестами: перечитать порядок побочных эффектов глазами, потому что тест на порядок обычно никто не писал.
  • Упало после удаления «мёртвого» кода → символ был живым. Вернуть, записать в журнал, идти дальше.
  • Упало то, что было красным в фазе 1 → так и сказать, со ссылкой на запись из фазы 1.
  • Тесты не запускаются (нет БД, нет сервисов) → прямо сказать, что проверки не было.

Phase 6. Отчёт

## Охват
<что смотрели; какие правила проекта прочитаны>

## Удалено
<мусор, отладка, закомментированный код; мёртвое — только если был --dead, с доказательством>

## Асинхронность
<что собрано в Promise.all и почему это безопасно; какие N+1 схлопнуты в один запрос>
<что оставлено последовательным — с причиной>

## Повторы
<объединённое; оставленные дубли и перечисления — с причиной; вынесенные значения и куда именно>

## Раскладка и единообразие
<перенесённое по ролям проекта; к какому большинству приведено меньшинство>

## Компоненты и стили
<что разделено «один файл — один компонент»; какая логика уехала и куда>
<что перенесено в общий пакет; что оставлено копией и почему>
<убранный свой CSS; значения, заменённые токенами — и что оставлено с обоснованием>

## Не тронуто
<группа C: генерация, миграции, чужие правки, горячие файлы, зеркала словарей>

## Отложено
<записано в журнал: ключ, причина, что вернёт вопрос; не переспрашивалось: N>

## Найдено, но не чинилось (группа D)
<баги, типизация, производительность — место и симптом, одной строкой>

## Разобрано
<по каждому классу: N из M; что осталось и почему>

## Проверки
<typecheck / build / тесты: было в фазе 1 → стало сейчас>

## Дифф
<+X / −Y строк; форматирование отдельным шагом: да/нет>

Never

  • Менять поведение под видом уборки — даже «очевидно правильно» исправляя баг по дороге.
  • Собирать await в Promise.all без разбора порядка побочных эффектов и семантики ошибки; ставить неограниченный Promise.all по коллекции неизвестного размера; распараллеливать внутри транзакции.
  • Заменять цикл с запросом на параллельный цикл, не проверив, можно ли сделать один запрос.
  • Сливать два перечисления, стоящие по разные стороны границы, через которую код не переносится.
  • Выносить в константу литерал, который встречается один раз и на месте читается лучше.
  • Навязывать раскладку и именование, которых в проекте нет: сначала правила проекта, потом господствующий в коде приём, и только потом мнение скилла.
  • Переименовывать файлы пачкой «под стиль» и совмещать перенос с переименованием.
  • Расширять охват за пределы просьбы: соседний файл, «заодно весь каталог», весь репозиторий.
  • Смешивать форматирование со смысловой правкой в одном диффе.
  • Запускать --fix/--write/--unsafe до утверждения плана.
  • Добавлять инструмент, конфиг или правило как побочный эффект уборки.
  • Удалять экспорт, потому что так сказал сканер: сначала снять export и спросить компилятор, а для ненадёжных зон — grep по строке и git log -S.
  • Удалять файл, помеченный как «вердикта нет» (динамика, глоб, реестр по строке).
  • Трогать сгенерированное, вендоренное, миграции и снапшоты.
  • Править файл с чужими незакоммиченными изменениями, не спросив.
  • Комментировать код вместо удаления и оставлять .bak-копии «на всякий случай».
  • Править комментарий там, где код не менялся.
  • Молча чинить поломки из раздела 1: компонент внутри компонента, хук под условием, список без key — это правка поведения, её сначала показывают.
  • Переписывать CSS в утилитарные классы без визуальной проверки и менять произвольные значения на ближайший шаг шкалы по своему усмотрению.
  • Переносить компонент в общий пакет, не согласовав: это правка публичного API пакета.
  • Делать вывод о классе находок по верхушке списка и по разбору нескольких случаев: «посмотрел четыре, остальные такие же» — это не проверка, а предположение.
  • Говорить «готово», пока в отчёте сканера стоит «ПОКАЗАНО НЕ ВСЁ» и раздел не разобран по --json.
  • Писать в отчёте «нет таких находок», когда на самом деле их не смотрели: «не найдено» и «не разбирали» — разные строки.
  • Ослаблять или удалять тест, чтобы дифф стал зелёным. Пропущенный тест — находка, а не мусор.
  • Переспрашивать то, что уже лежит в журнале с действующей причиной; и наоборот — оставлять отказ незаписанным.
  • Коммитить и пушить: коммит — отдельная явная просьба.
  • Докладывать об успехе, когда тесты не запускались или упали.

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.