Четыре месяца с Claude Code: что в итоге осталось лежать в .claude/

от автора

С 14 марта по 19 июля я писал в паре с Claude Code одно приложение: 517 коммитов, 228 из них релизные, 61 891 строка JS/CSS/HTML, 508 headless-тестов.

Сразу оговорюсь, потому что это объясняет половину дальнейшего текста: на работе я имею дело с базами данных и SQL. Фронтенд — не моя область. У меня нет насмотренности, которая позволяет опытному верстальщику глянуть на экран и сказать «тут поехал z-index». Всё, что описано ниже, — попытка заменить эту насмотренность формальными проверками.

Статья не о том, что «нейросеть написала мне приложение» — это давно неинтересно и обычно неправда. Она о другом: за четыре месяца вокруг агента наросла инфраструктура, которой в первый день не было и в помине. Пять скиллов, пять слэш-команд, четыре хука, 2066 строк собственных tools, отдельный каталог планов в памяти. Всё это появлялось не по плану, а как реакция на конкретные повторяющиеся факапы.

Ниже — четыре проблемы, которые пришлось решать инфраструктурно, и то, что из этого получилось. Плюс раздел про случаи, когда агент уверенно врал, и это было заметно не сразу.

Что за проект

Чтобы дальше было понятно, о чём речь: лист персонажа D&D 5e на русском. Vanilla JS, без сборщика и npm-зависимостей в рантайме, PWA с service worker, WebGL-кубики. Один HTML-файл, десяток JS-модулей по вкладкам, база на 719 заклинаний.

Технически важны две вещи, потому что они породили половину граблей:

  • Нет сборщика. Все скрипты подключены тегами с ?v= токенами.

  • Есть service worker, который агрессивно прекеширует файлы под именем CACHE_NAME.

Проблема 1: агент забывает шаги, которые забывать нельзя

Первое, что сломалось, — релизы.

Чтобы выпустить версию в этом проекте, надо синхронно изменить пять величин:

APP_VERSION  ↔  APP_CHANGELOG[0].version  ↔  CACHE_NAME (dnd-sheet-vN)             ↔  все ?v=vN токены js/css в index.html  ↔  CHANGELOG.md

Пропустил CACHE_NAME — service worker продолжает отдавать старый код, и правка «не поехала» у всех, кто уже открывал приложение. Пропустил ?v= токен на одном файле — поехала частично, что хуже: приложение собирается из старого и нового кода одновременно. Пропустил changelog — разъезжается версия в UI.

Первый месяц я просил об этом в промпте. Работает примерно в четырёх случаях из пяти, и это ровно та надёжность, которая хуже нуля: на пятый раз ты уже не проверяешь.

Решение оказалось скучным и правильным: если шаг нельзя забывать — его нельзя оставлять в промпте. Написал tools/bump-version.js, который делает все пять правок за один проход, и слэш-команду поверх:

/bump minor "CAST-11: добивка дескрипторов" --type feat

Внутри — то, до чего я дошёл не сразу. Первым делом скрипт лезет в origin:

function checkRemote() {  let remoteData, remoteSw;  try {    execFileSync('git', ['fetch', 'origin', 'main', '--quiet'], ...);    remoteData = execFileSync('git', ['show', 'origin/main:data.js'], ...).toString('utf8');    remoteSw   = execFileSync('git', ['show', 'origin/main:sw.js'],   ...).toString('utf8');  } catch (e) {    console.warn('WARN: пропуск remote-check (' + ... + ')');    return;              // нет сети/git — релиз офлайн не блокируем  }  // ...сверка локальных APP_VERSION / CACHE_NAME с remote}

Смысл в том, что версия — это не локальная величина. Если origin ушёл вперёд, а ты бампаешь от старой базы, получаешь дубль номера и разъехавшийся CACHE_NAME, который потом разгребается руками. Скрипт сверяет локальные значения с remote и падает с exit 1 и подсказкой git pull.

Обратите внимание на catch: отсутствие сети — не ошибка, это soft-fail с предупреждением. Граница «что блокирует релиз, а что просто ворчит» — единственное, что здесь требовало думать. Всё остальное механика.

Второй soft-fail — в самом конце: если генератор CHANGELOG.md упал, bump уже записан в три файла, и откатывать его хуже, чем оставить незаделанным. Скрипт честно говорит, чем чинить.

Вывод, который я бы принял на месяц раньше: любую многошаговую процедуру, где пропуск шага даёт тихую поломку, надо выносить в скрипт в тот же день, когда заметил пропуск. Не «попросить внимательнее» — вынести.

Проблема 2: напоминания не работают, работает exit 2

Скрипт закрывает случай «релиз запускаю осознанно». Он не закрывает случай «правлю sw.js посреди другой задачи и не думаю о релизе».

Тут пригодились хуки PostToolUse — команды, которые харнесс выполняет после каждой правки файла, и которые могут эту правку заблокировать, вернув exit 2. Ключевое: их исполняет не модель, а обвязка. Их нельзя «забыть», как нельзя забыть строчку в промпте.

Сейчас в .claude/settings.json их четыре, и они делятся на два класса.

Блокирующие (exit 2 — агент обязан отреагировать):

  1. sw.js guard. Тронул sw.js — хук читает оттуда текущий CACHE_NAME и печатает в stderr: «не забыл бампнуть? текущий=dnd-sheet-v274».

  2. data.js version sync. Если APP_VERSION разошёлся с APP_CHANGELOG[0].version — стоп. Это ровно то состояние, в котором в UI показывается одна версия, а в истории обновлений другая.

Предупреждающие (exit 0, просто пишут в лог):

  1. auto-tests — гоняет tests/headless-node.js на каждую правку *.js (кроме tests/, vendor/, assets/).

  2. theme-checktools/check-theme.js --hook на правки style.css.

Разделение не косметическое. Блокирующий хук на автотестах выглядит здравой идеей ровно до первого крупного рефакторинга: посреди него тесты закономерно красные несколько правок подряд, и хук превращает работу в борьбу с хуком. В warn-режиме он остаётся полезным — подсказывает, но не мешает.

Обратный случай — theme-check. Он навсегда warn-only локально и блокирующий в CI, и это записано в плане как отдельное решение. Локально дизайнерская правка часто временно ломает контрастный порог, пока подбираешь палитру. А вот в main такое попадать не должно.

Формулировка, к которой пришёл: блокировать надо необратимое и тихое, предупреждать — шумное и чинимое. Разъехавшаяся версия тихая: ты узнаёшь о ней от пользователя через неделю. Красные тесты шумные: ты и так их увидишь.

Проблема 3: каждый новый чат наступает на те же грабли

Самая дорогая проблема из всех.

Контекст чата конечен. Через четыре месяца у проекта накопились десятки нетривиальных особенностей, и в новом чате агент про них не знает. Хуже: он не знает, что не знает, — и уверенно делает наивную проверку, получает ложный результат и докладывает «всё работает».

Живой пример. Правлю CSS, прошу проверить в preview. Агент открывает страницу, смотрит — правки нет. Начинает чинить то, что не сломано.

Причина: service worker прекеширует файлы под текущим CACHE_NAME, а обработчик fetch матчит с ignoreSearch: true. То есть даже ?nocache= не спасает — SW отдаёт старую копию, игнорируя query-строку целиком. Пока не сбросишь регистрацию и кеши руками, ты смотришь на код недельной давности.

Первые разы я объяснял это в каждом новом чате. Потом завёл скилл — файл SKILL.md с описанием, который агент подтягивает сам, когда задача подходит под описание. verify-ui начинался как три абзаца про SW. Сейчас в нём восемь разделов, и каждый — след конкретного потерянного вечера:

Скриншот виснет. Страница непрерывно рендерит WebGL: орбитальный фон на requestAnimationFrame плюс dice-box на Babylon.js в воркере. Захват кадра ждёт idle, не дожидается и отваливается по таймауту. В скилле лежит рецепт: перед захватом убить рендер — отменить rAF, переопределить его на no-op, дёрнуть WEBGL_lose_context.loseContext() на всех canvas.

Computed-стиль врёт. Об этом отдельно ниже, это лучший баг за четыре месяца.

Скриншот, наоборот, врёт в другую сторону. JPEG-скриншот preview проходит авто-экспозицию: тёмный кадр вытягивается по яркости. Затемнение rgba(0,0,0,0.7) на светлой теме выглядит на скриншоте почти отсутствующим, хотя в браузере рендерится нормально. Рецепт в скилле: временно перекрасить маску в насыщенный красный — его экспозиция не съедает. Виден красный — работает и чёрный. Отдельно записано: «слабое чёрное затемнение на скриншоте само по себе багом не считать».

*/ внутри CSS-комментария. Вот эта находка стоила больше всего. Последовательность */ внутри комментария закрывает его досрочно. То есть безобидное

/* токены --cf-own-*/--cf-foreign-* для чужих карточек */

закрывает комментарий на -*/, хвост утекает в CSS, и error-recovery браузера молча роняет следующее правило. Симптом восхитительный: правило есть в файле, grep его находит, баланс скобок в порядке — а в CSSOM его нет, и computed-стиль подтягивается из менее специфичного правила. Ловится сканом stray-*/ или grep '\*/\S'.

Что здесь важнее самих рецептов: скилл — это не документация, это память о граблях. Документация описывает, как система устроена. Скилл описывает, где именно ты уже падал и как обойти. Первое агент во многом выведет из кода сам. Второе — не выведет никогда, потому что это знание отрицательное: «наивный способ проверки здесь даёт ложный ответ».

Правило, по которому я теперь дописываю скиллы: если я объяснил одно и то же в двух разных чатах — это строка в SKILL.md, а не третье объяснение.

Проблема 4: план не живёт дольше чата

Четвёртая проблема — масштаб. Задача «сделать так, чтобы кнопка „Использовать“ у заклинания реально применяла механику» в один чат не помещается никак.

Схема, которая в итоге прижилась: планы живут отдельными файлами в памяти, разбитые на фазы с кодовым префиксом. Шаблон минимальный:

### <X>-1: <Название> (~<время>) — **открыта**- <шаг>- Verify: <как проверить>

Новый чат стартует с триггера «начать фазу CAST-7». Агент читает файл плана, видит границы фазы и, что важнее, строку Verify — как именно проверять, что фаза закрыта. Закончили — /done CAST-7 проставляет статус.

Так план CAST прошёл 12 фаз за неделю: от «кнопка ничего не делает» до замкнутого боевого цикла — бросок атаки заклинанием, критический удвоенный урон, применение в цель боевого трекера, повторные тики по ходам, дебаффы чипом на участнике. Итог — кураторская таблица на 164 ключа, покрывающая 53 из 76 заклинаний с кубами.

Две вещи, которые я бы подчеркнул отдельно.

Первая: «не делаем» в плане важнее, чем «делаем». В шаблоне есть поле границ, и оно окупилось многократно. Без явной границы каждая фаза норовит расползтись в соседнюю область — там всегда есть что улучшить, и агент охотно улучшает.

Вторая: осознанно не покрытое надо записывать с причиной. В конце таблицы эффектов лежит комментарий-хвост: 23 заклинания не покрыты, разобраны по семи причинам. Без этого списка следующая фаза начнётся с вопроса «а почему тут дыра?» и попытки её закрыть — хотя дыра намеренная.

Где агент врал

Обещанный раздел. Три случая, отобранные по признаку «доклад был уверенный и неверный».

Случай 1: computed-стиль зелёный, картинка сломана.

Обучающий тур подсвечивает элемент интерфейса, затемняя всё вокруг. Классический приём — box-shadow с огромным spread на 9999px: вырезает «дырку» вокруг элемента, остальное затемняет.

На части машин затемнение просто не рендерилось. Диагностика показывала полную норму: getComputedStyle().boxShadow возвращает корректное значение, elementFromPoint подтверждает, что оверлей на месте и перехватывает клики. Все программные проверки зелёные. Визуально — затемнения нет.

Оказалось, GPU не рендерит spread такого размера. Значение в CSSOM корректное, элемент существует, композитор его игнорирует. Ни одна проверка через DOM или computed-стили этого поймать не может в принципе — они читают модель, а сломан рендер.

Починка — заменить одну «дырявую» тень на четыре сплошные панели вокруг подсвеченной области. В скилле с тех пор стоит: для оверлеев, затемнений и наслоений единственная достоверная проверка — реальный скриншот.

Мораль общая, не про CSS: агент проверяет то, что умеет прочитать программно. Если баг живёт ниже уровня, до которого дотягивается API, доклад будет честным по форме и ложным по сути.

Случай 2: аудит с ложными срабатываниями.

Прогнали автоматический аудит: сверить все заклинания, упомянутые в готовых билдах, с базой. Нашлось 10 «сирот» — имён, которых в базе нет.

Три оказались настоящими багами: недокат предыдущего переименования, где билд ссылался на старое имя. Починили.

Семь оказались не заклинаниями вообще. «Пламя» — часть названия монашеской дисциплины. «Помощь» — боевое действие Help. «Ошеломляющий удар» — классовая фича монаха. «Прорицание» — школа магии. Аудит матчил строки, а домен различает сущности, о которых строка ничего не говорит.

Это не ошибка агента в узком смысле — скрипт сделал ровно то, что просили. Это про то, что результат автоматического аудита в предметной области — не список багов, а список кандидатов. Разница в семь позиций из десяти. Если бы я «починил» все десять, я бы сломал четыре рабочих места ради трёх настоящих.

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

Случай 3: молчаливый недокат миграции.

Переименование заклинания прошло везде, кроме самой записи в базе. Миграция сейвов уже целилась в новое имя, UI показывал новое имя, а запись осталась под старым. Для сейвов определённых версий схемы это давало висячую ссылку.

Тесты были зелёные — они проверяли миграцию, а не консистентность базы с целями миграции. Нашлось при ручном проходе.

Отсюда привычка: после массового переименования проверять не «работает ли», а «на что теперь никто не ссылается и кто ссылается в пустоту». Это разные вопросы, и зелёные тесты отвечают только на первый.

Что в итоге лежит в .claude/

Спустя четыре месяца:

Что

Сколько

Зачем

Скиллы

5

Память о граблях: verify-ui, dice-3d, tours, release, add-content

Слэш-команды

5

/bump, /preflight, /test, /phase, /done

Хуки PostToolUse

4

2 блокирующих, 2 warn

Собственные tools

2066 строк

bump-version, check-invariant, check-theme, gen-changelog, аудиты

Тесты

508

Плюс CI на каждый push/PR

Ни один из этих файлов не был создан «правильно с самого начала». Каждый — реакция на конкретный вечер, потраченный на баг, который не должен был случиться дважды.

Если из статьи стоит унести одну мысль, то такую: работа с агентом на длинной дистанции — это не искусство формулировать промпты, а обычная инженерная работа по сокращению класса ошибок. Забыли шаг — скрипт. Забываете регулярно и молча — хук. Наступаете на грабли в каждом новом чате — скилл. Задача не влезает в контекст — фазовый план с явными границами и строкой Verify.

Промпт — это то, что вы говорите один раз. Всё перечисленное — то, что продолжает действовать, когда вы уже забыли, что об этом договаривались.


Приложение, на котором всё это обкатывалось, — бесплатный лист персонажа D&D 5e на русском: d1manych.github.io/dnd-app. PWA, работает офлайн, без регистрации, исходники открыты.

ссылка на оригинал статьи https://habr.com/ru/articles/1061696/