| name | program-implementation |
| description | Реализация программы по тикетам через Trunk Based Development. Применять, когда есть пакет проектной документации `docs/design/<slug>/` с зааппрувленным оператором хендофф-чеклистом, и нужно реализовать срезы по одному (один тикет = один slice = одна ветка = один PR). Не применять, если пакет неполный, чеклист не зааппрувлен оператором, или обнаружено противоречие в спецификации — остановиться и сообщить. |
program-implementation.skill — Реализация программы по тикетам, через Trunk Based Development
Назначение
Скилл для sonnet. На вход — пакет docs/design/<slug>/ от opus'а.
На выход — реализация программы, влитая в main по тикетам.
Метод: TBD + один тикет = один slice = одна ветка = один PR.
Зона ответственности
DO:
- Реализовывать ровно то, что в тикете.
- Идти TBD-циклом для каждого тикета.
- Останавливаться и сообщать, если спецификация неполная или
обнаружено противоречие.
DON'T:
- Менять контракты модулей без согласования с оператором.
- Реализовывать сразу несколько slice'ов в одной ветке.
- Делать «попутные улучшения», не относящиеся к тикету.
- Принимать архитектурные решения. Если возникает развилка — стоп, спросить.
Шаги одного тикета
Шаг 0. Проверить хендофф (выполняется один раз, при старте работы над пакетом)
Прежде чем брать первый тикет — открыть в main docs/design/<slug>/backlog.md,
найти раздел ## Хендофф-чеклист. Все пункты должны быть [x],
включая последнюю строку в строгом формате:
- [x] Оператор аппрувит пакет — @<github-handle>, <YYYY-MM-DD>
Эта строка — единственный детерминированный признак, что пакет принят.
Заполняется opus'ом при открытии дизайн-PR; акт мержа дизайн-PR =
аппрув оператора (если оператор не согласен с дизайном, он не мержит,
а оставляет ревью с правками; opus пересобирает пакет). Отдельной
post-merge ceremonии флипа нет — [x] уже стоит на момент создания PR
и переезжает в main вместе с мержем.
Без [x] на последней строке (или если строки нет) sonnet не начинает
работу, даже если все остальные пункты [x].
- Если чеклист отсутствует — пакет не готов. Остановиться, сообщить
оператору. Без чеклиста начинать нельзя: без согласованного
contracts-graph.md и зафиксированных контрактов sonnet будет
упираться в расхождения на каждом тикете.
- Если в main стоит
[ ] на последней строке аппрува — дизайн-PR
ещё не смержен либо opus забыл заполнить строку перед пушем.
Остановиться, сообщить оператору.
- Если в чеклисте есть
[ ] на любом другом пункте (не строка аппрува)
— opus сознательно оставил позицию незакрытой (расхождение со скиллом,
отложенное решение оператора и т.п.). Открыть карточку слайса,
раздел «Решения по дизайну» — там должно быть обоснование. Если
обоснования нет — остановиться, сообщить оператору.
- Если все
[x] и последняя строка содержит handle и дату — продолжаем
со Шага 1.
Этот шаг выполняется один раз на весь пакет, не на каждый тикет.
Шаг 1. Подтянуть main и создать ветку
git checkout main
git pull --ff-only origin main
git checkout -b feat/slice-<name>
Главное правило: новая ветка всегда от свежего main. Никаких ответвлений
от вчерашних веток.
Шаг 2. Прочитать спецификацию slice'а
docs/design/<slug>/slices/<n>-<name>.md — дерево модулей и контракты;
здесь же раздел ## Gherkin-mapping — таблица сверки Then-шагов
Gherkin-сценариев slice'а с узлами графа (Шаг 8.3 скилла opus'а)
docs/design/<slug>/messages.md — структуры данных
docs/design/<slug>/contracts-graph.md — граф вызовов и согласованность контрактов
component-tests/features/*.feature — компонентные сценарии slice'а
(исполняемая спецификация); читаются вместе с таблицей ## Gherkin-mapping
AGENTS.md, CLAUDE.md — конвенции репозитория
contracts-graph.md — основной источник истины о том, что чему передаётся
между модулями slice'а. Если в карточке модуля написано одно, а в графе
вызовов — другое, прав граф (он прошёл сверку opus'а).
Таблица ## Gherkin-mapping — карта от модулей к Then-шагам Gherkin.
Используется в Шаге 3 для прогона ровно тех сценариев, которые
зеленит реализованный модуль.
Если в спецификации обнаружено противоречие, недосказанность или
требование, нарушающее принципы дисциплины — остановиться и сообщить.
Не реализовывать «как-нибудь». Конкретные триггеры остановки:
- большой модуль, не помещающийся в одну фразу «что делает»;
- два I/O в одном модуле (одна внешняя зависимость + один режим работы
на модуль — см. Шаг 6 скилла opus'а);
- отсутствие антецедента или консеквента у модуля логики;
- рассогласование контрактов с графом (
contracts-graph.md);
- модуль с 2+ data-аргументами на входе (нарушение «жёсткого
правила одного аргумента» — Шаг 3 скилла opus'а). Признак: в
сигнатуре или в графе у узла больше одной стрелки-входа,
не считая deps. Признак-приметка в шаблоне Шага 5: строка
Input (data): содержит «и», «,» между сущностями. Это сигнал,
что opus пропустил введение доменной сущности — возврат на его
Шаг 3.
Особое правило про расхождения с графом контрактов. Если по ходу
реализации обнаружено, что сигнатура модуля по графу не стыкуется
с реальностью (возвращаемая структура не подходит вызывающему,
тип ошибки не разбирается, антецедент следующего модуля сильнее
консеквента предыдущего) — это архитектурное расхождение.
Не подгоняем код под граф ad hoc, не «починим в реализации» —
останавливаемся, сообщаем оператору, opus возвращается на свой
Шаг 9 и перепроверяет согласованность. После правки графа sonnet
продолжает.
Шаг 3. Реализация по формуле
Восходящий порядок (от листьев дерева к корню):
-
Структуры сообщений: Request (публичные поля, без правил) и
доменные структуры (Command, Entity) — с неэкспортируемыми
полями.
-
Конструкторы доменных структур — NewT(raw) -> (T, error).
Проверяют антецедент: невалидные данные → ошибка, структура не
создаётся. Это точка валидации slice'а; никакой валидации больше
нигде нет.
-
Модули логики slice'а — чистые функции над уже валидированными
доменными структурами (листья → узлы выше).
-
Модули I/O slice'а (по одному на каждую внешнюю операцию: чтение БД,
запись БД, публикация в брокер, вызов внешнего REST). Их может быть
несколько — это нормально, см. Шаг 6 скилла opus.
Правило автономного IO-объекта. Каждый I/O-модуль оборачивается
в объект, который инкапсулирует свою зависимость. Головной модуль
знает только методы объекта (API), не его зависимости. Имя объекта
отражает тип интеграции:
| Интеграция | Имя объекта | Зависимость |
|---|
| База данных | Store | *sql.DB |
| Внешний HTTP API | Client | *http.Client + baseURL |
| Брокер сообщений | Publisher / Consumer | соединение брокера |
Форма реализации (на примере Store):
type Store struct{ db *sql.DB }
func NewStore(db *sql.DB) Store { return Store{db: db} }
func (s Store) Save(msg DomainMessage) error { ... }
type Deps struct {
Store Store
Clock clock.Clock
}
Deps {
Deps{Store: NewStore(db), ...}
}
На конструкторах и модулях логики (шаги 2–3):
- сначала юнит-тесты по формуле (happy + ветки антецедента);
- затем реализация, до зелёных юнит-тестов;
- никаких лишних веток, не описанных в контракте.
Головной модуль (шаг 5), ингресс-адаптер (шаг 6) и I/O-модули (шаг 4)
юнит-тестами не покрываются. Головной модуль — склейка уже
протестированных частей; его корректность доказывается компонентным
сценарием через реальный вход. I/O и ингресс-адаптер — трубы,
проверяются компонентными тестами.
Никаких моков. Юнит-тесты работают только с реальными объектами
(чистые функции, доменные структуры). Если тест требует внешней
зависимости — это не юнит, это компонентный тест.
TDD outside-in поверх slice'а. Компонентный Gherkin-сценарий для
этого slice'а уже написан (опус-этап + скилл компонентных тестов
до начала реализации). До начала Шага 3 он красный — slice ещё
не реализован. По мере того как sonnet реализует модули по списку
выше, сценарий проходит от красного к зелёному.
Главный навигационный артефакт здесь — таблица ## Gherkin-mapping
в карточке slice'а (docs/design/<slug>/slices/<n>-<name>.md,
сделана opus'ом на Шаге 8.3). В ней для каждого Then-шага каждого
сценария явно указан узел графа (модуль / маппинг адаптера),
который этот Then зеленит. Sonnet использует таблицу как «карту
зелёного»: реализовал узел X — нашёл в таблице все строки с этим X —
прогнал ровно те сценарии, которые эти строки покрывают.
Алгоритм:
- Реализован конструктор / модуль логики → найти в таблице строки
с этим узлом → прогнать соответствующие Gherkin-сценарии. Если строк
нет — узел не виден в Gherkin (нормально для чистых конструкторов;
их зеленят юниты).
- Реализован I/O-модуль (Success-ветка) и подключён в головном модуле
slice'а → запустить happy-path сценарии, в чьих строках этот I/O
фигурирует. Должны позеленеть.
- Реализована Failure-ветка I/O-модуля + соответствующий маппинг
в ингресс-адаптере → запустить сценарий отказа, в чьих строках стоит
этот класс ошибки. Должен позеленеть.
- К концу Шага 3 все строки таблицы
## Gherkin-mapping закрыты,
все компонентные сценарии этого slice'а зелёные.
Это даёт детерминированную обратную связь: «реализовал X → должен
позеленеть сценарий Y» прямо из таблицы, без догадок.
Если компонентный сценарий зелёным не становится, а юнит-тесты модулей
зелёные и таблица ## Gherkin-mapping говорит, что узел реализован —
проблема не в коде, а в спецификации: либо контракт slice'а
не соответствует тому, что обещано в OpenAPI/AsyncAPI, либо
contracts-graph.md не учёл какой-то путь, либо строка таблицы
указывает на несуществующий узел. Стоп, сообщить оператору,
opus возвращается на Шаг 8.3 / Шаг 9 проверить согласованность.
Если юнит-тест красный потому, что в самом тесте ошибка — править тест.
Если контракт явно противоречит реальности — стоп, сообщить (см. Шаг 2).
Конструкторы доменных структур — главный объект юнит-тестирования
slice'а. Они инкапсулируют все правила валидации, по формуле получают
«1 happy + по тесту на каждую ветку антецедента». Модули логики —
чистые функции, тестируются по той же формуле (обычно только happy,
если антецедент тривиален). Головной модуль, ингресс-адаптер и
инфраструктурный модуль юнит-тестами не покрываются — только
компонентные тесты через реальный вход.
Шаг 4. Прогнать локальный CI
Локальный CI — четыре шага, зеркалящие .github/workflows/ci.yml.
Все четыре обязательны до отчёта оператору. Пропустить любой = CI на PR упадёт.
unformatted=$(gofmt -l .); [ -n "$unformatted" ] && echo "$unformatted" && exit 1
go vet ./...
go test ./...
./component-tests/scripts/run-tests.sh healthy
Не останавливаться после шага 3 — компонентные тесты обязательны.
Не пропускать шаги 1–2 — они первые в CI и валят PR независимо от тестов.
Критерий зелёного состояния перед ревью:
- gofmt: пустой вывод (нет неформатированных файлов).
- go vet: нет замечаний.
- Юнит-тесты: все зелёные.
- Компонентные тесты: зелёные сценарии текущего slice'а и всех
ранее реализованных slice'ов. Красные — только сценарии ещё не
реализованных slice'ов (ожидаемые 404 на не подключённых роутах).
Красный сценарий реализованного slice'а — стоп, разобраться.
Если юнит-тест красный:
- ошибка в реализации модуля → править реализацию, не трогая контракт;
- ошибка в самом тесте (опечатка, неверное ожидание) → править тест;
- контракт модуля противоречит вызывающим модулям → стоп, сообщить
(см. правило про расхождения в Шаге 2).
Если компонентный тест красный на сценарии текущего slice'а:
- модули реализованы, но slice не складывается → проверить головной
модуль и ингресс-адаптер;
- сценарий ожидает поведение, не описанное в спецификации → стоп,
сообщить, opus возвращается на Шаг 9 (
contracts-graph.md).
Шаг 5. Отметить тикет в backlog.md
- [ ] компонентные тесты режимов отказа зелёные
+ [x] компонентные тесты режимов отказа зелёные
Чек-лист обновляется по каждому подтверждённому пункту, не одним батчем
в конце. Backlog — единственный источник истины о статусе.
Шаг 6. Записать в devlog
docs/design/<slug>/devlog.md — журнал реализации, по одному блоку
на тикет, в порядке закрытия. Sonnet добавляет блок текущего тикета
после того как локальный CI стал зелёным, и до подготовки
коммита (чтобы devlog попал в этот же коммит).
Формат блока:
## S<n> — <идентификатор входа> (<YYYY-MM-DD>)
**Что сделано:** одна-две фразы по сути изменения.
**Решения, принятые по ходу:** локальные решения, которые не меняют
архитектуру, но которые читателю через год полезно знать. Если
решений не было — пропустить раздел.
**Что застряло:** если sonnet останавливался на расхождении и opus
правил `contracts-graph.md` — короткое описание ситуации. Если
не застрял — пропустить раздел.
**Тесты:** юниты <число>, покрытие <%>. Компонентные сценарии:
<имена сценариев>, все зелёные.
Devlog — для людей, не для CI. Длина блока — 5–15 строк.
Шаг 7. Pre-push гейт: самопроверка дисциплины + сводка оператору
Прежде чем пушить ветку наружу — последний sanity gate. Sonnet прогоняет
четыре grep-самопроверки (правила дисциплины), готовит сообщение коммита
и собирает короткую сводку в чат. Ждёт «пушим» от оператора.
Цель этого гейта — поймать дисциплинарные нарушения и опечатки в
commit-message до публичности (push гонит CI; отозвать commit-сообщение
после push'а — это force-push, что само по себе церемония). Полный diff
в терминале не выводим — он есть в PR, и GitHub UI лучшая поверхность
для построчного ревью. В терминале — только сводка.
7.1. Grep-самопроверка дисциплины
Выполнить четыре команды. Каждая должна вернуть пустой вывод.
Непустой вывод = нарушение = исправить до сводки. Без исключений.
grep -rn "database/sql\|net/http\|\"io\"\|amqp\|kafka" internal/slice/*/head.go
grep -rn "^func Test" internal/slice/*/*_test.go \
| grep -iE "Process|Handle|Head|Orchestrat"
grep -rn "^type.*struct{}" internal/slice/*/*_test.go \
| grep -iv "testclock\|testClock"
grep -n "func(" internal/slice/*/register.go
Типичная ловушка: тесты головного модуля с реальным in-memory SQLite
кажутся «честными» (не мок же!), но нарушают правило — головной модуль
не тестируется юнитами независимо от того, реальные ли зависимости.
Компонентный сценарий уже покрывает этот путь через реальный HTTP-вход.
7.2. Подготовить сообщение коммита
Атомарный, в формате <type>(<scope>): <subject> (Conventional Commits,
см. AGENTS.md §12):
feat(slice-<name>): реализовать <идентификатор входа>
- ингресс-адаптер <name>
- модули <перечисление: конструкторы, логика, I/O>
- юнит-тесты по формуле, покрытие 100%
- компонентный тест happy + <режимы отказа> зелёные
- backlog.md обновлён, devlog.md дополнен
Closes S<n>
Где <идентификатор входа> — то же, что в заголовке тикета: например,
HTTP POST /v1/registrations, Broker registrations.created,
gRPC RegistrationService.Create, CLI registrations:cleanup.
7.3. Сводка оператору + ожидание «пушим»
В чат — короткая сводка для финальной проверки. Без полного diff'а.
Слайс готов к push'у.
Файлы:
- <ключевые изменённые/созданные файлы>
Локальный CI (все четыре шага):
- gofmt: чисто
- go vet: чисто
- Юниты: <число> зелёных
- Компонент: <число> сценариев Gherkin зелёных
- Дисциплина (grep 1-4): чисто
Сообщение коммита:
<блок коммита из 7.2>
Жду «пушим» / правки.
Sonnet ждёт ответа оператора. Дальнейшие действия:
- «пушим» / «ок» / «push» / любое явное согласие → переход на Шаг 8
(commit + push + PR).
- Замечания (по grep'у, тексту коммита, перечню файлов) → sonnet правит,
повторяет 7.1-7.3.
- Молчание / уточняющий вопрос → отвечать, не пушить.
Шаг 8. Commit, push, открыть PR, уведомить
После аппрува «пушим» в Шаге 7 — sonnet выполняет атомарную
последовательность без дальнейших остановок:
git commit -m "..."
git push -u origin feat/slice-<name>
gh pr create --base main --fill
Шаблон описания PR:
## Тикет
S<n> — slice <name>: <идентификатор входа>
## Что сделано
<пункты Definition of Done, отмеченные галочками>
## Спецификация
docs/design/<slug>/slices/<n>-<name>.md
## Тесты
- юниты: <число>, покрытие <%>
- компонентные: <число> сценариев Gherkin зелёные
## Чек-лист TBD
- [x] ветка от свежего main
- [x] локальный CI зелёный
- [x] backlog.md обновлён
- [x] devlog.md дополнен
После того как gh pr create вернул URL — короткое уведомление в чат
(не вопрос; мерж — право оператора, видит PR и решает сам; см.
skills/program-design/SKILL.md Шаг 12 «merge = аппрув»).
СТОП. Агент не мержит PR самостоятельно — никогда.
gh pr merge вызывается только по явной команде оператора («мердж», «влей», «merge»).
«пуш», «ок», CI зелёный — не основание для мержа.
PR #<номер> открыт:
Что сделано: <1-2 фразы>.
Ключевые файлы: <карточка слайса, головной модуль, тесты>.
Переход на Шаг 9.
Шаг 9. Дождаться мержа + подтянуть main + следующий тикет
Мерж делает оператор кнопкой Merge в GitHub-интерфейсе. К моменту
мержа должны быть выполнены оба условия: оператор нажал merge и
CI на PR зелёный.
- Если CI красный — sonnet ремонтирует коммитами в ту же ветку, не новой
веткой и не на main.
- Если оператор оставил замечания в PR — sonnet правит в той же ветке
(возвращается на Шаг 3 или дальше — туда, где правка релевантна),
пушит, обновляет PR. Оператор смотрит снова и решает, мержить ли.
После мержа:
git checkout main
git pull --ff-only origin main
Тикет считается закрытым, когда CI на main зелёный после мержа.
Sonnet возвращается на Шаг 1 для следующего тикета (backlog.md →
следующий тикет с выполненными зависимостями → новая ветка от свежего
main).
Definition of Done скилла
Применительно ко всему пакету:
- Все тикеты в
backlog.md отмечены [x].
- main зелёный.
- Все компонентные сценарии Gherkin зелёные.
- Devlog
docs/design/<slug>/devlog.md заполнен по тикетам.