10 min read
Мой AI-workflow

Введение

Удобный, но хрупкий режим — реализовать фичу в одной чат-сессии: сначала обсудить, потом написать код, потом чинить CI в том же окне. Контекст быстро раздувается, а на очень длинном окне (100k токенов и выше) модель хуже помнит ранние договорённости и чаще ошибается.

Главный тезис этой статьи простой: агент справляется там, где решения уже приняты и записаны, контекст каждой итерации мал, а состояние работы живёт вне агента — в issue-трекере или файлах, а не в его памяти. Ниже — план, по которому можно провести фичу от идеи до PR с учётом этих ограничений.

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

Workflow

Мой workflow по большей части основан на подходе Matt Pocock и его наборе скиллов: последовательность шагов от идеи до финального PR там уже заложена, я опираюсь на неё как есть.

Разберу каждый шаг на примере небольшого пет-проекта geo-quiz. Стек у меня завязан на Claude Code и GitHub, но сами приёмы не про конкретный инструмент: их можно повторить с любым ИИ-агентом для работы с кодом (например, OpenCode) и с локальными файлами вместо GitHub issues.

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

Пример реализуемой фичи

В приложении нужно добавить экран настроек (/settings): игрок задаёт количество вопросов в раунде — пресеты (10, 25, все страны каталога) или своё число; выбор сохраняется в localStorage и применяется при старте следующего раунда.

Settings screen

Фиксация требований

На этом этапе у Matt Pocock есть два связанных скилла:

  • /grill-me — агент опрашивает по плану, проходя по веткам дерева решений, пока каждая не разрешится.
  • /grill-with-docs — то же интервью, но дополнительно агент сверяет план с существующей доменной моделью, уточняет терминологию и по ходу обновляет CONTEXT.md и ADR (Architectural Decision Record, запись об архитектурном решении).

Если речь про проект с кодовой базой — почти всегда стоит брать /grill-with-docs. Исторически первым был /grill-me — скилл, который сам по себе стал вирусным. Но в процессе работы Matt заметил, что ему регулярно не хватало общего языка с агентом (в DDD это ubiquitous language, UL): на грилл-сессиях всплывали хорошие термины, но они нигде не фиксировались, и в следующий раз их приходилось проговаривать заново. Сначала он запускал параллельно второй скилл /ubiquitous-language, который вытаскивал термины в отдельный глоссарий, а потом объединил оба в один — так появился /grill-with-docs.

В geo-quiz я запускаю именно его. Копия скилла лежит в репозитории — /grill-with-docs. Дальше всё стандартно: описываю агенту идею, вызываю скилл, и он начинает выяснять детали и фиксировать каждое значимое решение. Параллельно агент правит CONTEXT.md в корне репозитория.

CONTEXT.md — это глоссарий проекта: в нём живут определения ключевых терминов. Идея заимствована из DDD: один из его центральных концептов — ubiquitous language, единый язык, на котором говорят три стороны — код, разработчики и доменные эксперты. Когда все трое называют одни и те же вещи одинаково, исчезает класс ошибок: в разговоре сущность зовётся одним словом, в задаче — другим, в коде — третьим.

Для агента эффект ровно такой же, как для нового человека в команде: вместо того чтобы каждый раз заново выяснять, что такое, например, «раунд» или «пресет», он сверяется с CONTEXT.md и сразу использует готовый термин. Имена в коде и в PR совпадают с глоссарием, ответы агента становятся короче.

В больших репозиториях единый файл глоссария может перестать справляться: один и тот же термин в разных частях системы может обозначать разные сущности с разными правилами. На этот случай в DDD есть bounded context — явная граница, внутри которой язык согласован. У Matt для таких репозиториев в корне лежит CONTEXT-MAP.md: он не содержит определений сам, а только показывает, какие контексты есть в проекте и где у каждого свой CONTEXT.md.

/
├── CONTEXT-MAP.md
├── docs/adr/                # системные решения
└── src/
    ├── ordering/
    │   ├── CONTEXT.md
    │   └── docs/adr/        # решения внутри контекста
    └── billing/
        ├── CONTEXT.md
        └── docs/adr/

В небольшом проекте всё это избыточно — достаточно одного CONTEXT.md в корне. В большом продукте CONTEXT-MAP.md оправдан: он подсказывает агенту (и человеку), в какой глоссарий идти за термином, и не даёт смешать языки разных контекстов.

Кроме CONTEXT.md, /grill-with-docs иногда предлагает завести ADR — отдельный markdown-файл в docs/adr/. ADR заводится, если выполняются три условия: решение тяжело откатить, оно выглядит странно без контекста, имеет какой-то компромисс. Таких документов сознательно мало.

Создание PRD

Когда глоссарий устоялся и спорные решения зафиксированы в ADR, идея готова к переводу в формальный документ — PRD (Product Requirements Document).

PRD — это короткий документ, описывающий одну фичу: цель, сценарии использования, scope (что входит и что не входит), acceptance criteria. В отличие от CONTEXT.md, который описывает язык домена в целом, PRD всегда привязан к одной конкретной фиче.

PRD создаётся скиллом /to-prd — этот скилл берёт контекст чата и создаёт PRD по шаблону и далее публикует его на GitHub. Пример такого PRD: issue #13 — Configured round size (settings page).

Сам документ в репозитории не хранится — он живёт только как GitHub issue. После создания PRD я поверхностно просматриваю получившийся артефакт. Matt в своём видео говорит, что не делает этого: общий контекст с агентом, по его словам, уже достигнут на этапе /grill-with-docs, и PRD здесь нужен скорее как точка фиксации, чем как объект для согласования.

Нарезка задач

Скилл /to-issues разбивает большой PRD на небольшие задачи. Каждая из этих задач:

  • Самодостаточная — можно взять, реализовать и смерджить.
  • Это vertical slice (tracer bullet) — тонкий разрез через все слои (модель, сервис, UI, тесты), а не «весь backend сначала, потом весь frontend».
  • Помечена как AFK (можно отдать агенту) или HITL (требует человека — например, дизайн-решение или архитектурный выбор).

Агент показывает предлагаемое разбиение, получает фидбек, итерирует и потом публикует дочерние issue на GitHub с лейблом ready-for-agent — по этому лейблу позже их забирает ralph loop (см. ниже).

На все issue, относящиеся к одному PRD, вешаются лейблы:

  • prd — этот лейбл получает только сам PRD-issue.
  • prd-<N>, где <N> — номер PRD-issue. Этот лейбл получает и сам PRD, и все его дочерние issue.

Такая схема нужна, чтобы потом одной выборкой найти все дочерние issue, относящиеся к PRD.

Пример одного такого «ребёнка»: issue #14 — Configured round size: Quiz preferences store and round-start resolution.

После этого шага у меня в GitHub лежит: один PRD-issue, к нему привязаны несколько дочерних issue, у всех нужные лейблы.

ralph loop

ralph loop — это очень простой цикл:

while not done:
    agent_step()

Название придумал Geoffrey Huntley — это отсылка к Ralph Wiggum из «Симпсонов». Персонаж туповатый, но упорный: делает одно и то же и не унывает от неудач. Цикл ведёт себя так же — один и тот же промпт, разный код вокруг, и так пока задача не будет закрыта.

Ralph Wiggum

У меня для этого есть скрипт .agents/ralph/loop.sh: он в цикле вызывает агента по одному и тому же PROMPT.md и смотрит на последнюю строку вывода — STATUS=done, STATUS=progress или STATUS=blocked. По статусу решает, выходить или звать агента снова.

Под капотом скрипт принимает номер PRD-issue, переходит в корень репозитория и в цикле до MAX_ITERS (по умолчанию 20) запускает агента с одним и тем же PROMPT.md. Вывод каждой итерации пишется в .agents/ralph/logs/<timestamp>/iter-NN.log — оттуда же скрипт грепает последнюю строку STATUS=… и решает, продолжать ли цикл.

Сам вызов агента:

claude \
  --allowed-tools=Bash,Read,Edit,Write,MultiEdit,Grep,Glob \
  -p "$PROMPT_BODY"

Флаг -p (он же --print) переводит Claude Code в headless-режим: агент печатает ответ в stdout и завершается, без интерактивного TUI:

~ ❯ claude -p "Hello, claude"
Hello! How can I help you today?
~ ❯

Без него вызов из скрипта не имел бы смысла — TUI заблокировал бы пайплайн.

Если вместо Claude Code использовать OpenCode, эту строку меняем на opencode run "$PROMPT_BODY" — у него ровно тот же неинтерактивный режим, который не ждёт ввода и сам автоапрувит инструменты. Остальная обвязка (логи, парсинг STATUS, цикл) остаётся как есть.

Рядом с loop.sh лежит loop-once.sh — почти тот же скрипт, но без внешнего цикла и без -p. Без -p агент запускается в обычном интерактивном TUI: я вижу, как он рассуждает, какие инструменты дёргает, где спотыкается. Это режим для наблюдения и тюнинга. На практике им я пользуюсь чаще, чем полным loop.sh — это и есть «части вместо целого» из оговорки в начале.

Принципиально важная деталь: между итерациями ralph ничего не помнит. Он берёт ровно один дочерний issue и реализует его. На следующем шаге всё начинается заново, с пустого контекста: агент перечитывает GitHub, видит, какие issue уже закрыты, какие заблокированы, и сам решает, что брать следующим.

Состояние я держу в GitHub: открытые/закрытые issue, лейблы, открытые/смердженные PR. Это единый источник истины. Если ralph упал посреди работы, то его можно просто перезапустить, он сам разберётся, на каком шаге продолжать.

Не обязательно использовать GitHub в качестве хранилища состояния. Точно так же можно хранить задачи в локальной директории tasks/ с markdown-файлами, где статус указывается во frontmatter, а агент сканирует эту папку вместо gh issue list. Ключевая идея — чтобы состояние сохранялось вне памяти агента, в устойчивом хранилище, которое переживает перезапуск.

Внутри каждой итерации ralph использует мой скилл implement-issue.

Этот скилл:

  • Сверяется с CONTEXT.md и ADR’ами — чтобы названия в коде/коммитах/PR совпадали с доменом.
  • Создаёт ветку ralph/<n>-<slug> от epic-ветки PRD (prd/<N>-<slug>), а не от main.
  • Реализует issue через TDD — это отдельный скилл tdd. Используется vertical slice: один тест → минимальная имплементация → следующий тест. Acceptance criteria из тела issue становятся списком тестов.
  • Прогоняет локально lint, prettier, build, vitest.
  • Открывает PR в epic-ветку с Closes #<n>.
  • Передаёт PR скиллу babysit, который добивает его до зелёного CI и сквош-мержит в основную PRD-ветку.

Все дочерние PR сливаются в epic-ветку prd/<N>-<slug>. Финального слияния в main ralph никогда не делает — это моя зона ответственности.

Финальный PR в main

Когда ralph закрывает последнего ребёнка PRD, у меня в GitHub лежит один большой epic-PR prd/<N>-<slug>main, состоящий из аккуратной серии сквош-коммитов «один коммит на issue».

Пример PR в epic-ветку: PR #18 — Configured round size: Settings page and home navigation.

Дальше:

  • Прохожу функциональность глазами и руками в браузере.
  • Читаю diff целиком: меня интересует, как фича выглядит как единое целое
  • С помощью агента делаю правки там, где агент срезал угол или повернул не туда
  • После QA — мержу epic в main. GitHub автоматически закрывает все дочерние issue по Closes #<n> и сам PRD.

Если коротко

Полный путь от идеи до мержа в main:

  1. /grill-with-docs — агент опрашивает по плану, фиксирует язык и спорные решения в CONTEXT.md и ADR.
  2. /to-prd — собирает контекст чата в PRD и публикует его как GitHub issue с лейблом prd.
  3. /to-issues — режет PRD на дочерние vertical-slice issue и вешает routing-лейблы (prd-<N>, ready-for-agent).
  4. ralph loop — берёт ready-for-agent issue, через implement-issue доводит её до PR в epic-ветку; babysit добивает CI и сквош-мержит.
  5. Финальный мерж — я руками прохожу epic-PR в браузере, читаю diff целиком, точечно правлю и мержу в main.

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