almdev Технический блог

Агент готовит ветку для PR без меня
Выйти из цикла реализации можно только тогда, когда заранее описано, где остановиться.

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

Подкатегория: Лаборатория

Чтение
5 мин
Технологии / версии
Coding agent · PHP · Лаборатория · Git
Серия
Лаборатория · часть 9 из 9
1 сен 2026 · 5 мин · 10 просмотров · AI
Автономный агент проходит ограниченный маршрут до готовой локальной ветки
Лаборатория 09/2026

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

Девятый заход серии. Проект тот же: Symfony 8, 96 тысяч строк, 830 тестов, CI на 8 минут.

Задача для автономного прогона

Выбирал специально: достаточно большая, чтобы автономность имела смысл, и достаточно очерченная, чтобы у неё был проверяемый критерий.

Метки на задачах. Сущность метки, привязка многие-ко-многим, управление в интерфейсе проекта, фильтр по метке в списке задач, отображение на карточке. Примерно 400 строк в знакомом паттерне: миграция, сущность, связь, интерфейс и фильтр.

Что важно: у задачи нет архитектурных развилок. Всё, что нужно решить, уже решено в проекте на других сущностях. Автономный прогон на задаче с развилкой я пробовал позже, и это был один из двух выброшенных прогонов.

Три слоя ограничителей

Автономность — это не «разрешить всё». Это заранее описанные границы, внутри которых можно не спрашивать.

Слой первый: разрешения на инструменты. Что агент может делать без вопроса, а что требует подтверждения.

{
  "permissions": {
    "allow": [
      "Bash(make test)",
      "Bash(make check)",
      "Bash(git add *)",
      "Bash(git commit *)",
      "Bash(git checkout -b *)",
      "Edit(src/**)",
      "Edit(tests/**)",
      "Edit(templates/**)",
      "Edit(migrations/**)"
    ],
    "deny": [
      "Bash(git push *)",
      "Bash(git reset --hard *)",
      "Bash(git rebase *)",
      "Bash(docker compose down *)",
      "Bash(php bin/console doctrine:database:drop *)",
      "Bash(php bin/console doctrine:schema:drop *)",
      "Edit(.env*)",
      "Edit(config/packages/security.yaml)",
      "Edit(phpstan-baseline.neon)"
    ]
  }
}

Запреты интереснее разрешений. Отправка в удалённый репозиторий закрыта — PR создаю я командой после просмотра. Жёсткий сброс и перебазирование закрыты, потому что они уничтожают историю, а история автономного прогона — единственный способ понять, что там происходило.

Базовая линия статического анализа закрыта отдельной строкой. Без этого запрета у агента есть простой способ сделать проверку зелёной: дописать туда новое замечание.

Слой второй: хуки. То, что должно происходить всегда, независимо от решений агента. Ниже фрагмент конфигурации инструмента на момент эксперимента; названия событий и схема файла зависят от установленной версии, поэтому копировать его без сверки с документацией нельзя.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [{ "type": "command", "command": "make format" }]
      }
    ]
  }
}

В этом Symfony-проекте цель make format запускает принятый проектом PHP-форматтер. Форматирование после каждой правки убирает из финального диффа шум по стилю. Насколько именно оно ускоряет чтение, я не замерял; практическая разница была заметна глазами.

Слой третий: условия остановки. Самый важный и единственный, который пишется словами.

Условия остановки

ОСТАНОВИСЬ И СПРОСИ, ЕСЛИ:
- нужно изменить публичный контракт: сигнатуру существующего сервиса,
  схему существующей таблицы, формат существующего сообщения очереди
- нужна новая зависимость в composer.json
- нужно тронуть модуль Core
- тест, который был зелёным до начала работы, стал красным и причина
  не очевидна из твоего же диффа
- ты сделал три попытки исправить одно и то же и не вышло
- задача оказалась больше 600 строк диффа

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

Последний пункт про откат добавлен после первого выброшенного прогона. Агент упёрся, решил, что зашёл не туда, и откатил четыре часа собственной работы до чистого состояния. Формально разумно. Практически я потерял возможность посмотреть, где именно он свернул.

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

Гигиена коммитов

Автономный прогон легко превращается в один коммит на 400 строк с сообщением «реализована функция меток». Такой PR невозможно ревьюить.

Требование в постановке:

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

Каждый коммит оставляет проект в рабочем состоянии: make test проходит.
Сообщение: что изменилось и зачем, без перечисления файлов.

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

Что получилось

Прогон занял 2 часа 40 минут почти без моего участия: один раз я ответил на условие остановки. На выходе была готовая локальная ветка:

  • 6 коммитов, 430 строк, 14 файлов
  • 21 новый тест, все зелёные
  • make check проходит, базовая линия не выросла
  • одна остановка с вопросом

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

Я ответил тремя строками, он продолжил. Общее время моего участия — четыре минуты.

Два выброшенных прогона

Честно про то, что не сработало.

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

Через три часа я получил рабочую реализацию не той архитектуры. Выбросил.

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

Второй. Тот, где агент откатил свою работу. Из него выросло правило «ничего не откатывай при остановке».

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

Чего я не отдаю

Отправку в удалённый репозиторий и создание PR. Не из недоверия к коду, а потому что описание PR — это документ для человека, и его пишу я, посмотрев на дифф.

Слияние. Даже при зелёном CI.

Решение, что задача сделана. Агент отвечает на вопрос «выполнены ли критерии», а не на вопрос «решена ли задача». Это разные вопросы, и второй остаётся мне.

Замер

ПоказательЗначение
Время прогона2 ч 40 мин
Моё участие4 мин
Дифф430 строк, 6 коммитов
Остановок с вопросом1
Правок на ревью3 мелких
Выброшенных прогонов из семи2

Три правки на ревью — переименование метода, лишний индекс в миграции и один тест, дублирующий соседний. Мелочь на пятнадцать минут.

Что дальше

Локальная ветка готова и зелёная. После просмотра я создаю PR; в следующем, последнем заходе серии он поедет в CI, и выяснится, что «сделай зелёным» — самая опасная формулировка из всех, что были в этой серии.

Серия

Лаборатория

#coding-agent #php #lab #git #avtonomnost