С 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 — агент обязан отреагировать):
-
sw.js guard. Тронул
sw.js— хук читает оттуда текущийCACHE_NAMEи печатает в stderr: «не забыл бампнуть? текущий=dnd-sheet-v274». -
data.js version sync. Если
APP_VERSIONразошёлся сAPP_CHANGELOG[0].version— стоп. Это ровно то состояние, в котором в UI показывается одна версия, а в истории обновлений другая.
Предупреждающие (exit 0, просто пишут в лог):
-
auto-tests — гоняет
tests/headless-node.jsна каждую правку*.js(кромеtests/,vendor/,assets/). -
theme-check —
tools/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 |
|
|
Хуки 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/