ИИ-агент собирает админку каталога по одной инструкции

Шесть частей цикла собрали библиотечный каталог с закрытой админкой на Vue, читательскими аккаунтами, фильтрами с честными ошибками, обложками из медиатеки, двумя языками и заказом в типографию по подписанному вебхуку. Всё это существует в живом проекте, и всё это кто-то заводил: типы контента, сценарии, маршруты, шаблоны. Пришло время рассказать, кто именно заводил их в демо и почему следующую фичу вы можете не заводить руками вовсе.
В этой части каталогу выдачи книг оформляет ИИ-агент — тот же класс инструментов, что Claude Code, Cursor или Qwen Code. Человек пишет инструкцию на одну страницу: какой тип контента нужен, какие эндпоинты, какая панель в интерфейсе. Дальше агент читает пакет документации платформы и работает командной строкой. Заводит запросы, собирает сценарии, публикует маршруты, перебирает ошибки по журналу прогонов. Предыдущие части цикла: сборка закрытой админки, читательские аккаунты, фильтры и честные ошибки, обложки из медиатеки, два языка из одной записи, внешний сервис и вебхук.
Отдельного «встроенного агента» в платформе нет — и не нужно. Достаточно того, что весь сайт доступен через API, а правила работы с ним собраны в пакет, который понимает любой внешний инструмент.
Пакет документации, ключ, командная строка
У агента три вещи, и все три выдаёт платформа.
Пакет dynapi-site-dev — инструкции в формате скилла: когда и как заводить типы, из чего собирается сценарий, чем читается сайт, где ловятся ошибки. Живёт по адресу dynapi.ru/cms/agent-skill/, оттуда его читает человек, скачивает архивом agent-skill.zip или кладёт в свой редактор кода как справочник проекта. Внутри — восемь документов: рабочие процессы, справочник GraphQL API, каталог действий и условий сценария, правила Liquid, механика member-аккаунтов. Тем же пакетом пользуется документация для людей: в разделе про ИИ-агентов те же правила изложены по-человечески.
Ключ проекта — dca_…, создаётся в админке в разделе «Ключи доступа». Для сборки сайта хватает областей site:manage, data:write и media:write; ключ сам определяет, какому проекту принадлежит запрос. Ключ не коммитится и не вставляется в тексты сценариев — он живёт в переменной окружения DYNAPI_APIKEY.
flowctl — командная строка, скачивается из того же пакета (agent-skill/cli/flowctl-linux-amd64, есть сборки под Windows и macOS). Адрес GraphQL-эндпоинта в неё зашит, поэтому настройка — это передать ключ. Внутри — entity для типов контента и записей, query для библиотеки запросов, validate и dryrun для проверки сценариев до публикации, apply для импорта, route для карты маршрутов, files push для шаблонов и статики, runs для журнала прогонов, media и secret для библиотеки и секретов.
Инструкция на одну страницу
Качество сборки определяется качеством инструкции. Хороший документ описывает результат, а не клики: «тип такой-то с такими-то полями, эндпоинты с такими контрактами, панель с таким поведением». Остальное — правила работы и явный список «за кадром», чтобы агент не раздувал объём самодеятельностью.
Инструкция для выдач книг выглядит так, и дальше по тексту агент исполняет её буквально:
# Задача для агента: выдачи книг в каталоге
Проект — каталог небольшой библиотеки на `biblio.dynapi.ru`.
Работаешь по скиллу `dynapi-site-dev`; инструмент — `flowctl`, ключ в
переменной `DYNAPI_APIKEY`. Ничего не изобретай: контракты действий и
условий — в справочниках скилла, и всё опубликованное проверяй живьём.
## Что нужно построить
Читатели берут книги на руки. Добавь каталогу выдачи: один тип контента,
четыре сценария-эндпоинта и раздел «Выдачи» в админке каталога.
### 1. Тип «Выдача» (loan)
| поле | тип | правила |
|----------|----------------------------------------------|--------------|
| book | запись → Книга | обязательное |
| member | запись → Читатель | обязательное |
| due_date | дата | обязательное |
| status | выбор: active «На руках» / returned «Возвращена» | обязательное |
Схему заведи в редакторе схемы или мутацией `createEntity`
(api-reference §4). Прежде чем писать сценарии, дождись пересборки схемы:
`flowctl entity schema loan` должен показать все четыре поля.
### 2. GET /api/members — читатели для селекта
members-only. JSON: `{"members":[{"id","name","email"}…]}`, по алфавиту имён.
Запрос сохрани в библиотеку запросов под именем `members_list`.
### 3. GET /api/loans — активные выдачи
members-only. JSON: `{"loans":[{"id","due_date","book":{title},"member":{email}}…]}`.
Только `status = active`, по возрастанию `due_date`. Запрос — в библиотеку
под именем `loans_list`.
### 4. POST /api/loans/issue — выдать книгу
members-only. JSON-тело: `{"book_id", "member_id", "due_date"}`.
Проверки по очереди, у каждой свой ответ 400 с именем поля:
`book_id` и `member_id` — UUID, `due_date` — дата `ГГГГ-ММ-ДД`.
Успех: `createLoan` со `status: active` → 201 `{"loan":{…}}`.
Если записи с таким id нет — 400 `{"error":"REF_NOT_FOUND"}`.
Мутацию можно оставить инлайн: библиотека — про чтение страниц,
не про разовые действия.
### 5. POST /api/loans/return — принять книгу обратно
members-only. JSON-тело: `{"loan_id"}`; UUID-проверка → 400 с именем поля.
`updateLoan(status: returned)`. Выдачи с таким id нет →
404 `{"error":"NOT_FOUND"}`, успех → 200 `{"ok":true}`.
### 6. Раздел «Выдачи» в админке (spa/)
Под каталогом — панель «Выдачи»: таблица активных выдач (книга, читатель,
«вернуть до», кнопка «Вернул»). Сама выдача — из строки каталога: кнопка
«Выдать» раскрывает в строке селект читателя и дату возврата. Ошибки — как в
форме книг: баннер с сообщением сервера и подсветка поля. После выдачи и
возврата перечитывай список. Обе локали словаря (RU и EN). Пересобери бандл,
запушь статику (`flowctl files push`) и подними `?v=` в `admin.liquid`.
## Правила работы
- Цепочка на каждый сценарий: `validate` → `dryrun` (пустое тело, мусор в
полях, счастливый путь) → `apply` вместе с маршрутом → живая проверка →
`runs`. Dry-run мокает graphql-действие — честный ответ сценария
проверяется только живым вызовом и журналом прогонов.
- Маршруты members-only: JSON-клиент без сессии получает 403, а не редирект.
- Ключ не попадает в файлы репозитория и в тексты сценариев.
- Секретов в этой задаче нет; если появятся — только `secret://`, не инлайн.
## За кадром (не делаешь)
- Одна книга — одна активная выдача: проверку не вводим.
- Уведомления читателям: в демо нет SMTP.
- История возвратов: выданные и возвращённые показываем только активные.

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

Инструкция разрешает и второй путь — мутацию createEntity с рецептом из api-reference: для агента разница только в том, куда нажимать. flowctl entity schema loan дожидается пересборки схемы и печатает все четыре поля.
Дальше — сценарии. Каждый проходит цепочку «собрал → проверил → опубликовал». Оффлайн-проверка validate ловит синтаксис Liquid и инварианты графа. Репетиция dryrun исполняет несохранённый документ против мок-запроса. Пустое тело, мусор в полях, счастливый путь — по три-четыре прогона на сценарий, каждый ответ виден до публикации. Для выдачи книги цепочка условий выглядит так:
{
"id": "c_book",
"type": "condition",
"data": {
"predicate": {
"combinator": "and",
"clauses": [
{ "field": "json.book_id", "operator": "matches",
"value": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }
]
}
}
}
Три такие условия по очереди проверяют book_id, member_id и формат даты, каждый провал ведёт в свой ответ 400 с именем поля. За ними graphql-узел создаёт выдачу со status: "active", финальное условие смотрит в results и выбирает между 201 и REF_NOT_FOUND. Публикация — flowctl apply; маршрут остаётся черновиком, поэтому следом идёт flowctl route save --publish. Канва сценария после сборки.

Чтение — members_list и loans_list — уезжает в библиотеку запросов, как требует скилл: страница читает данные по имени записи библиотеки, и правка запроса потом делается в одном месте. Разовые мутации агент оставил инлайн — инструкция это явно разрешила.
Живой прогон ловит то, что репетиция мокает
Первый живой клик по «Выдать» вернул ошибку. Репетиция была зелёной по всем веткам, но dry-run не исполняет graphql-узел по-настоящему — он подкладывает ответ из мок-контекста. Журнал прогонов показал точную причину:
graphql: graphql errors: Argument "input" has invalid value
{book: $book, member: $member, dueDate: $due, status: "active"}.
In field "dueDate": Unknown field. In field "due_date": Expected "String!", found null.;
Cannot query field "dueDate" on type "Loan". Did you mean "due_date"?
Агент назвал поле в camelCase — dueDate, а схема отдаёт поле под слагом как есть — due_date. Правка коснулась текста запроса в узле, той же строки в записи loans_list и выборки поля в ответе. Перепубликация, живой прогон — и выдача оформилась.

Виновата слепая вера в зелёную репетицию, агенты ни при чём. Инструкция потому и требует живую проверку после публикации: dry-run отвечает на вопрос «верно ли расставлены ветки», журнал прогонов — «что реально сказал GraphQL».
Проверка человеком
Агент закончил — слово за человеком. В админке у книги появилась кнопка «Выдать», раскрывающая строку с селектом читателя и датой возврата:

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

Живая проверка прошла по всем веткам, не только по счастливой: выдача без читателя вернула 400 с подсветкой селекта, возврат убрал строку из панели, JSON-запрос без сессии получил 403 AUTHENTICATION_REQUIRED вместо редиректа. Каталог из статьи живой — biblio.dynapi.ru, читатель reader@biblio.test с паролем chitalka-2026, выдачи можно попробовать самому.
Что у агента отобрано
Платформа сознательно обходится без встроенного агента. Агент вне платформы — значит, он работает с теми же правами, что вы ему выдали, и ни одним больше. Ключ с тремя областями не умеет менять тариф, удалять проект и читать чужие проекты. Ключ отзывается одной кнопкой в том же разделе — сессия агента закончилась, доступ закончился.
Решения остаются за человеком, и инструкция это фиксирует. Что строить и зачем — написано человеком. Что НЕ строить — перечислено в «за кадром». Секреты агенту не выдают в тексте задачи — для них в платформе одностороннее хранилище из прошлой части, и в этой задаче их не было вовсе.
Собранный агентом код проходит обычную ревью-дорожку: всё, что он сделал, лежит в ветке step-10-loans репозитория демо — тип, четыре сценария, записи библиотеки, обновлённое Vue-приложение и та самая INSTRUCTION-LOANS.md. Читается как обычный пул-реквест.
На этом цикл замкнут. Семь частей описали один путь: типы контента превращаются в GraphQL-схему, сценарии — в страницы и API, сайт живёт в одном origin, а сборку можно доверить и человеку, и агенту. Обе руки — ваши.
Что осталось за кадром
flowctl entity create умеет наполнять типы записями — сиды каталога в репозитории залиты именно им. Пакет скилла обновляется вместе с платформой, и страницы agent-skill собираются из того же репозитория, что и код, и попадают в sitemap как документация.
Для задач побольше инструкция разрастается в план с этапами и проверками — механика та же: результат, контракты, правила, «за кадром». История с due_date — лучший кандидат на добавку в справочник скилла: поля в GraphQL называются ровно как слаги, и camelCase там не живёт.
Все записи