Loop Engineering на практике: автономный цикл «оркестратор, мейкер, чекер»
Разбираем рабочий демо-стенд loop engineering: внешний цикл находит задачу в GitHub Issues, изолирует работу в Git worktree, делегирует реализацию мейкеру и независимую проверку чекеру и доводит задачу до pull request – с валидацией по правилам и памятью в state/loop-state.md.
В отдельной статье мы разбирали loop engineering как подход: внутренний и внешний циклы, паттерн maker-checker, Git worktrees, память и критерии успеха. Теперь перейдём от теории к практике и соберём небольшой, но рабочий стенд, где внешний цикл сам управляет агентами.
Это демо метода loop engineering, которую популяризировал Addy Osmani, – с моими собственными изменениями и дополнениями. Система находит задачи в GitHub Issues, изолирует их в worktree, делегирует реализацию и независимую проверку разным агентам и действует через внешние инструменты. Мейкер и чекер – это субагенты; их разделение и обеспечивает независимую проверку.
Процесс намеренно простой, но, как показала практика, эффективный. Задачи для агентов хранятся в GitHub Issues – это встроенный в GitHub таск-трекер. Агент-оркестратор загружает задачу через MCP, создаёт изолированный Git worktree и делегирует работу субагентам. Дальше всё происходит без ручного промптинга.
Как устроен процесс
Логика внешнего цикла укладывается в одну схему. Оркестратор берёт задачу, изолирует работу, передаёт её мейкеру, отправляет результат на проверку чекеру и по итогу проверки либо создаёт pull request, либо фиксирует неудачу в файле памяти. Третьего не дано: проверка либо пройдена, либо нет.
Разберём этот процесс по шагам.
Требования
Для запуска понадобятся:
- Oh My Pi (
omp) – обвязка для запуска агента. Установка:npm install -g @ohmy-pi/cli. Подойдёт и другой агент (OpenCode, Pi, Claude Code) – принципиальной разницы для этого подхода нет. - Node.js
>= 18и npm. - Git с поддержкой worktree.
- Персональный токен доступа GitHub (personal access token) со скоупами
repoиissues– только для боевого режима.
Режимы запуска
Стенд работает в двух режимах.
Демо-режим (без учётных данных GitHub)
DEMO_MODE=true ./scripts/loop-demo.sh
Задачи берутся из fixtures/demo-issues.json. Всё состояние и имитация действий с PR и задачами пишутся в state/loop-state.md. Сетевых вызовов нет.
Боевой режим (нужен токен GitHub)
GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx ./scripts/loop-demo.sh
Система находит открытые задачи с меткой loop-demo, обрабатывает по одной за запуск, создаёт pull request и закрывает задачу.
Что происходит за один цикл
Один проход цикла состоит из пяти шагов.
- Discover (найти). Находит первую открытую задачу с меткой
loop-demo– вfixtures/demo-issues.json(демо-режим) или через GitHub API (боевой режим). - Isolate (изолировать). Создаёт Git worktree с веткой
fix/<issue-key>. - Delegate (делегировать мейкеру). Запускает субагента-мейкера: тот читает задачу, реализует исправление в worktree и прогоняет
npm testиnpm run build. - Verify (проверить чекером). Запускает субагента-чекера: тот выполняет
scripts/validate-fix.sh– скрипт автоматической проверки по правилам, который сверяет тесты, сборку, корректность реализации и критерий приёмки задачи. - Act (действовать). Если проверка пройдена – создаётся PR и закрывается задача (боевой режим) либо записывается имитация действия (демо-режим). Если проверка не пройдена – фиксируется причина отказа и выполняется очистка.
Структура проекта
В качестве агента я использую Oh My Pi. Конвенции проекта описаны в .omp/AGENTS.md: как собирать проект, какие команды запускать, каких ограничений придерживаться. Вот как выглядит репозиторий целиком.
.omp/
AGENTS.md – конвенции проекта для всех агентов
mcp.json – MCP-сервер: GitHub (issues + PR)
mcp.json.template – шаблон для подстановки токена
agents/maker.md – определение субагента-мейкера
agents/checker.md – определение субагента-чекера
skills/loop-demo/SKILL.md – логика оркестратора + общие конвенции
demo-app/
src/calculator.ts – цель: багованный add() [a - b вместо a + b]
tests/calculator.test.ts – падающий тест: add(2, 3) === 5
package.json, tsconfig.json, .gitignore, package-lock.json
scripts/
loop-demo.sh – точка входа (окружение, проверки, запуск omp)
validate-fix.sh – скрипт автоматической проверки по правилам (PASS/FAIL)
fixtures/
demo-issues.json – заготовки задач для DEMO_MODE=true
state/
loop-state.md – здесь оркестратор фиксирует прогресс
// [ERROR fetch failed] eugeneproai/le-demo/main/.omp/AGENTS.md
// [ERROR fetch failed] eugeneproai/le-demo/main/.omp/mcp.json
Субагенты: мейкер и чекер
Роли строго разделены. Мейкер (.omp/agents/maker.md) получает задачу от оркестратора, реализует её в worktree и проверяет себя, запуская npm test и npm run build, после чего возвращает результат оркестратору. Он не создаёт pull request, не обновляет задачу в GitHub, не запускает скрипт валидации и не трогает файлы, не связанные с задачей.
// [ERROR fetch failed] eugeneproai/le-demo/main/.omp/agents/maker.md
Чекер (.omp/agents/checker.md) решает не менее простую задачу: получает входные данные от оркестратора, запускает скрипт-валидатор с нужными параметрами и возвращает результат. Вариантов ровно два – PASS или FAIL, то есть проверка пройдена или нет. Если пройдена – оркестратор создаёт pull request и меняет статус задачи. Если FAIL – pull request не создаётся, статус задачи не меняется, а в state/loop-state.md появляется пометка, что задача не прошла валидацию.
// [ERROR fetch failed] eugeneproai/le-demo/main/.omp/agents/checker.md
Почему в центре оркестратор
Цикл управляется оркестратором: один агент прогоняет весь цикл, порождая субагентов для мейкера и чекера. Мейкер и чекер никогда не видят работу друг друга – они запускаются как независимые субагенты, и между ними только оркестратор. Именно поэтому проверка получается по-настоящему независимой.
Оркестратор → найти задачу → создать worktree
├─ запустить Мейкера (реализует исправление, гоняет тесты)
└─ ждать
├─ запустить Чекера (независимо запускает скрипт валидации)
└─ ждать
├─ PASS → создать PR, закрыть задачу, обновить state
└─ FAIL → записать причину, удалить ветку, обновить state
Приложение для демонстрации
Приложение максимально простое – калькулятор. Цель – файл demo-app/src/calculator.ts, где функция сложения сразу реализована с ошибкой: вместо сложения чисел выполняется вычитание (a - b вместо a + b).
// [ERROR fetch failed] eugeneproai/le-demo/main/demo-app/src/calculator.ts
К приложению есть один готовый тест demo-app/tests/calculator.test.ts, который проверяет операцию сложения: add(2, 3) === 5. Из-за бага он падает – багованная функция возвращает -1 вместо 5.
// [ERROR fetch failed] eugeneproai/le-demo/main/demo-app/tests/calculator.test.ts
Отдельно лежит файл fixtures/demo-issues.json – на случай, если цикл запускается в демо-режиме, без GitHub Issues и MCP. Тогда агент берёт задачи прямо из этого файла.
// [ERROR fetch failed] eugeneproai/le-demo/main/fixtures/demo-issues.json
Скрипт валидации
Самое интересное – скрипт валидации scripts/validate-fix.sh, который запускает чекер. Он выполняет несколько шагов по порядку.
Сначала запускаются тесты. Затем – сборка проекта. И, наконец, самый неочевидный шаг: проверяется, что критерий приёмки действительно реализован в коде. Причём это не только тот критерий, который явно указан в задаче.
Допустим, в задаче указано, что критерий приёмки – это пройденные тесты и определённый результат конкретной функции. Но в самом скрипте критерий – это результат интерпретации всей задачи с помощью LLM. Например, для операции сложения скрипт ищет в нужном файле именно операцию сложения, то есть a + b. Если такая операция найдена – критерий приёмки считается выполненным.
// [ERROR fetch failed] eugeneproai/le-demo/main/scripts/validate-fix.sh
Если все шаги завершились успешно, валидатор возвращает PASS. Если проверка не пройдена на любом из шагов – FAIL. Этот скрипт – единственный источник правды о правилах валидации: чекер не выдумывает правила, а запускает скрипт и передаёт его вывод дословно.
⚠️ Проверка критерия приёмки через LLM удобна для демо, но это не гарантия корректности. Автоматическая проверка по правилам и явные тесты остаются обязательными – финальную ответственность за результат всё равно несёт разработчик.
Запуск внешнего цикла
Запуск оформлен bash-скриптом scripts/loop-demo.sh. Строк в нём много, но все они нужны, чтобы перед стартом основного агента пройти предварительные проверки: доступ к MCP, наличие нужных файлов и так далее.
// [ERROR fetch failed] eugeneproai/le-demo/main/scripts/loop-demo.sh
Сам агент запускается в неинтерактивном режиме, без подтверждения действий от пользователя. Пользовательский промпт короткий: использовать скилл loop-demo для запуска полного цикла, получить задачу, делегировать работу мейкеру и чекеру и действовать по результатам так, как описано в скилле. Сам скилл .omp/skills/loop-demo/SKILL.md содержит логику оркестратора, ссылку на конвенцию проекта, информацию о репозитории и все те шаги, что описаны выше.
// [ERROR fetch failed] eugeneproai/le-demo/main/.omp/skills/loop-demo/SKILL.md
Что получилось после запуска
После старта поднимается оркестратор, и мы ждём выполнения. По итогам первого прогона создаётся state/loop-state.md – файл памяти. В нём три раздела: выполненные задачи, задачи с ошибками и задачи, над которыми идёт работа прямо сейчас. Файл обновляется по мере реализации задач. На первой итерации агент исправляет функцию сложения.
Обновляем страницу в GitHub – создался pull request с этой задачей. Смотрим коммит: всё исправлено, вместо операции вычитания теперь сложение, все условия соблюдены. Открытой осталась только вторая задача – реализовать деление чисел.
Любопытная деталь: в тестах изначально была только проверка результата сложения, а теста для второй задачи (деления) не было. Вообще этого лучше не допускать и писать тесты самостоятельно, но раз уж это демонстрация – посмотрим, напишет ли агент тест для второй задачи сам.
Запускаем цикл ещё раз, для второй задачи. Она тоже выполняется: файл calculator.ts обновлён, файл с тестами обновлён, чекер вернул PASS, проверки пройдены. Критерий приёмки – деление a / b – реализован, все нужные файлы обновлены, статус задачи изменён. Причём агент добавил все возможные тесты – с этой частью он справился даже аккуратнее человека.
Смотрим Issues – открытых задач нет, две закрытые. У каждой есть комментарий, коммит и pull request, задача закрыта.
Проверка результата
Чтобы убедиться, что стенд собран правильно и цикл отработал, есть несколько проверок.
Предварительная проверка
cd demo-app && npm test # тест не пройдет: add(2, 3) возвращает -1 вместо 5
cat .omp/mcp.json # должен содержать запись github MCP
ls .omp/AGENTS.md .omp/agents/maker.md .omp/agents/checker.md .omp/skills/loop-demo/SKILL.md scripts/validate-fix.sh
Сквозная проверка (демо-режим)
DEMO_MODE=true ./scripts/loop-demo.sh
less state/loop-state.md # DEMO-1 должен появиться в разделе ## Completed
ls ../le-demo-2-wt-DEMO-1/ # каталога быть не должно (worktree удалён)
Сквозная проверка (боевой режим)
- Создайте в репозитории задачу с заголовком «Fix add() returning subtraction result» и меткой
loop-demo. - Запустите баш-скрипт
GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx ./scripts/loop-demo.sh. - Проверьте: задача закрыта, есть pull request из ветки
fix/<issue-numbering>вmain.
Как сбросить состояние
rm -rf ../le-demo-2-wt-*
git branch -D fix/DEMO-1 2>/dev/null || true
git worktree prune
rm -f state/loop-state.md
Вывод
С помощью такого цикла можно реализовывать любые задачи. В этом демо они намеренно простые, но ничто не мешает адаптировать метод под задачи сложнее и масштабнее. На ежедневных задачах агент во внешнем цикле при правильной настройке показывает себя хорошо.
Главное – держать процесс в понятных границах: атомарные задачи, проверяемые критерии, независимая проверка результата и файл памяти, который хранит состояние между итерациями. Тогда внешний цикл не превращается в чёрный ящик, а остаётся управляемым инструментом.


