Полный цикл с агентом: от спецификации к коду
Сквозной практикум на записи на потоки онлайн-школы: конституция, спецификация и уточнения, план в режиме планирования, задачи, реализация короткими итерациями с хуком на тесты, субагентом-ревьюером и коммитом после каждой задачи. Что делает человек, пока агент работает, и как возвращать агента к спецификации.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Есть шутливое описание SDD: хорошенько пошевелить мозгами на этапе планирования, родить тонну md-файлов, а затем скормить их агенту и спокойно идти пить кофе, пока Claude перемалывает ваши спеки в рабочий код. В этой шутке правда наполовину. Продуманная спецификация действительно позволяет отдать агенту много работы. Но если уйти на три часа, то, вернувшись, вы найдёте 1500 строк, которые никто не читал. В них агент «заодно» переименовал половину классов и поправил тест, который ему мешал.
За предыдущие модули у вас собрались все детали. Вы знаете, как устроен агент для кодаИИ-агент для кодаEN AI coding agentПрограмма, в которой языковая модель в цикле читает и правит файлы проекта, запускает команды и тесты и сама решает, что делать дальше: Claude Code, GitHub Copilot, Cursor, Codex и др. и его контекст (урок SDD.1.2). Умеете писать требования и сценарии (SDD.2.1), план (SDD.2.2), задачи и конституцию (SDD.2.3). Видели, как цикл автоматизирует Spec Kit (SDD.3.1). Этот урок собирает детали в один рабочий процесс. Мы возьмём спецификацию записи на поток из урока SDD.2.1 и доведём её до кода, принятого тестами. По дороге увидим, где кофе пить можно, а где нельзя отходить от экрана.
Примеры даны для Claude Code, но процесс не привязан к инструменту: режим планирования, файлы памяти и субагенты есть и у других агентов.
Карта цикла
Заголовок раздела «Карта цикла»flowchart LR K["Конституция<br/>(один раз)"] --> S["Спецификация"] S --> U["Уточнения"] U --> P["План<br/>(режим планирования)"] P --> T["Задачи"] T --> I["Задача: тесты → код"] I --> R["Ревью диффа<br/>человеком"] R -->|"не так"| I R -->|"принято"| C["Коммит"] C -->|"следующая задача"| I C -->|"задачи кончились"| V["Проверка и сведение<br/>(урок SDD.4.2)"] U -.->|"человек отвечает"| S P -.->|"человек одобряет"| T
Главное на схеме — контрольные точки человека. Между ними агент работает сам, на них работает человек.
| Шаг | Артефакт | Делает | Человек проверяет |
|---|---|---|---|
| Конституция | specs/constitution.md | человек, агент помогает | принципы проверяемы, их мало |
| Спецификация | spec.md | агент пишет черновик | каждое требование — одно обязательство, у каждого есть сценарий |
| Уточнения | правки spec.md | агент спрашивает, человек отвечает | маркеры [УТОЧНИТЬ] закрыты или явно вынесены |
| План | plan.md | агент в режиме планирования | интерфейс, файлы, решения не противоречат конституции |
| Задачи | tasks.md | агент | задачи маленькие, у каждой есть требования и сценарии |
| Реализация | код, тесты, коммиты | агент, по задаче за раз | дифф каждой задачи |
| Проверка | отчёт о трассируемости | агент и скрипты | все требования покрыты, нет лишнего кода |
Работа человека приходится на начало (спецификация и план) и на ревью, а писать код — дело агента. На учебной фиче весь цикл занимает два-три часа.
Подготовка: проект, конституция, память агента
Заголовок раздела «Подготовка: проект, конституция, память агента»Структура учебного проекта
Заголовок раздела «Структура учебного проекта»school/├── CLAUDE.md # память агента: команды, где что лежит, порядок работы├── .claude/│ ├── settings.json # хук: тесты перед концом каждого ответа│ ├── hooks/run_tests.py│ └── agents/spec-reviewer.md # субагент: сверка диффа со спецификацией├── docs/glossary.md # единый язык (урок DDD.1.2)├── specs/│ ├── constitution.md│ └── 001-enrollment/│ ├── spec.md # из урока SDD.2.1 + уточнения│ ├── plan.md│ └── tasks.md├── enrollment/ # контекст «Набор»│ ├── __init__.py│ └── model.py├── tests/acceptance/test_enrollment.py└── tools/next_task.py # подсказчик следующей задачи (практика)Это «свой процесс» без инструментов из урока SDD.3.3. У Spec Kit
папки называются иначе: конституция лежит в .specify/memory/constitution.md, а команды установлены как навыки
агента. Шаги при этом те же.
Конституция
Заголовок раздела «Конституция»Конституцияконституция проектаEN project constitutionТермин Spec Kit: файл с неизменными принципами проекта (в Spec Kit — .specify/memory/constitution.md), по которым проверяются план, задачи и код: тесты сначала, простота, стандарты кода, запреты. Меняется редко, через явную поправку с номером версии. — короткий список принципов, по которым проверяется всё остальное. Для учебного проекта хватит шести:
# Конституция проекта «Онлайн-школа» (v1.0)
1. Спецификация — источник правды. Код делает то, что написано в spec.md, и ничего сверх. Расхождение — стоп и вопрос человеку; сначала меняется спецификация, потом код.2. Тесты сначала. Каждый сценарий SC-NNN — приёмочный тест test_sc_NNN_…, написанный до кода. Падает тест — правим код, а не тест.3. Единый язык: имена только из docs/glossary.md.4. Время в модели — только параметром now; деньги — Money (копейки и валюта).5. Только стандартная библиотека и pytest; новая зависимость — с согласия человека.6. Одна задача — один коммит «T-NNN: … (REQ-…)»; после каждой задачи все тесты зелёные.Каждый пункт проверяем тестом, хуком, линтером или чтением диффа. «Пиши качественный код» проверить нельзя — такого пункта нет.
Память агента
Заголовок раздела «Память агента»Файл памятифайл памяти проектаEN project memory file (CLAUDE.md, AGENTS.md)Markdown-файл в репозитории, который агент сам читает в начале каждой сессии: команды сборки и тестов, правила, глоссарий, где лежат спецификации. Claude Code читает CLAUDE.md; открытый формат AGENTS.md понимают многие агенты. CLAUDE.md (или AGENTS.md) вы писали в уроке SDD.1.2. Для
цикла в него стоит добавить раздел о порядке работы — тот, что агент должен перечитывать в каждой сессии:
## Порядок работы по спецификации- Правила проекта — specs/constitution.md. Читай перед каждой задачей.- Работаем по одной задаче из specs/<фича>/tasks.md и только когда я её назвал.- Порядок в задаче: тесты сценариев → показать, что падают → код → все тесты → git diff --stat.- Коммиты делаю я. «Статус: готово» в tasks.md ставишь ты после зелёных тестов.- Нашёл противоречие или пробел в спецификации — остановись и спроси. Не додумывай.Шаг 1. Спецификация и уточнения
Заголовок раздела «Шаг 1. Спецификация и уточнения»Спецификация уже есть — это пример «запись на поток» из урока SDD.2.1: десять требований REQ-001…REQ-010 и
девять сценариев SC-001…SC-009. Два вопроса глоссария в ней уже закрыты строками «Решено» со ссылкой на урок
DDD.2.3: повторная запись после отмены разрешена, а платёж,
пришедший после автоматической отмены, возвращается — это забота Оплаты. Два маркера [УТОЧНИТЬ] (перенос потока
и частичный возврат) явно вынесены за рамки.
Кладём спецификацию в specs/001-enrollment/spec.md и запускаем цикл уточнений промптом из урока SDD.2.1. Агент
находит новые вопросы, и это нормально: свежий взгляд на готовый документ всегда что-то находит.
Агент: 1. REQ-006: «вернуть полную стоимость» — а если запись ещё не оплачена (статус «ожидает оплаты»)? А) возврат 0, денег не было (рекомендую); Б) правило не применяется. 2. Что делать, если студент отменяет уже отменённую запись? А) ошибка InvalidStatusTransition, как в модели урока DDD.3.3 (рекомендую); Б) ничего не делать.Вы: 1 — А, 2 — А. Обнови требования и добавь сценарии.Первый вопрос нашёл настоящую дыру: по тексту REQ-006 неоплаченная запись даёт «полный возврат» денег, которых не было. Агент, который не спросил бы, так бы и реализовал. Итог уточнений — дифф спецификации:
-- **REQ-006**: Когда студент отменяет запись за 7 суток (7 × 24 часа) или более до старта потока, система должна вернуть ему полную стоимость.+- **REQ-006**: Когда студент отменяет подтверждённую запись за 7 суток (7 × 24 часа) или более до старта потока, система должна вернуть ему полную стоимость.+- **REQ-011**: Если запись на поток уже отменена, то система должна отклонить повторную отмену с ошибкой InvalidStatusTransition.
### SC-010: Отмена неоплаченной записи — без возвратаТребования: REQ-006### SC-011: Повторная отменаТребования: REQ-011Шаги «Дано / Когда / Тогда» в диффе сокращены. Заметьте: новое требование получило следующий свободный номер REQ-011, а не вклинилось между старыми, а новые сценарии — SC-010 и SC-011. Идентификаторы никогда не перенумеровывают. На них ссылаются задачи, тесты, коммиты и обсуждения, и перенумерация молча сломала бы всю трассируемостьтрассируемостьEN traceabilityВозможность пройти по ссылкам от требования к сценарию, задаче, тесту и коду и обратно. Держится на идентификаторах (REQ-001, SC-001, T-001) и позволяет автоматически найти требования без задач и задачи без требований..
Шаг 2. План в режиме планирования
Заголовок раздела «Шаг 2. План в режиме планирования»Режим планированиярежим планированияEN plan modeРежим агента, в котором он только читает код и предлагает план, ничего не меняя, пока человек план не одобрит. В Claude Code переключается сочетанием Shift+Tab; похожие режимы есть и у других агентов. в Claude Code включается сочетанием Shift+Tab (нажимайте, пока в
строке состояния не появится ⏸ plan mode on) или запуском claude --permission-mode plan. В этом режиме агент
читает файлы и предлагает план, но ничего не меняет, пока вы план не одобрите. Промпт:
Прочитай specs/constitution.md, specs/001-enrollment/spec.md и docs/glossary.md.Составь технический план фичи по шаблону из урока SDD.2.2: модули и файлы, публичный интерфейс(классы, методы, исключения, события — имена только из глоссария), алгоритм каждого правиласо ссылкой на REQ, как будем тестировать. Отдельно — проверка по каждому принципу конституциии список вопросов ко мне. После одобрения сохрани план в specs/001-enrollment/plan.md.Фрагмент плана, который получился:
## Интерфейс: enrollment/model.py- Cohort(cohort_id, starts_at, capacity, price)- enroll(student_id, now) -> enrollment_id # REQ-001…REQ-004- confirm(enrollment_id, now)- expire_unpaid(now) # REQ-005: now - enrolled_at >= 48 ч- cancel(enrollment_id, now) -> Money # REQ-006, 007, 011: starts_at - now >= 7 × 24 ч- pull_events() -> [EnrollmentCancelled, ...] # REQ-008## Вопросы- Публиковать ли SeatReleased? Предлагаю: нет, лист ожидания — отдельная фича.План читайте не как текст, а как список решений. Что проверить:
- Интерфейс. Имена из глоссария, время — параметром, деньги —
Money. - Границы. Как сравнивается время в правилах 48 часов и 7 суток (
>=). Здесь агенты ошибаются чаще всего. - Лишнее. Нет ли в плане базы данных, HTTP, «гибкой системы правил», которых не просили. Принцип простоты обрезает это сразу.
- Вопросы. Если их ноль, перечитайте план внимательнее: в реальной фиче вопросы есть всегда.
Ошибку в плане исправить стоит одну реплику, а в коде — дифф, ревью и откат. Режим планирования настолько окреп, что Agent OS в версии 3.0 (январь 2026) отказался от собственных команд для написания спецификаций и передал эту работу режиму планирования агента (урок SDD.3.3). Но план в окне чата исчезнет вместе с сессией. Поэтому последняя фраза промпта и просит сохранить его в файл.
Шаг 3. Задачи
Заголовок раздела «Шаг 3. Задачи»Из плана агент делает tasks.md в формате курса из урока SDD.2.3: заголовок ## T-NNN: название, обязательные
строки «Требования» и «Зависит от», желательная «Готово, когда». Для работы с агентом добавим два необязательных поля.
«Сценарии» прямо связывают задачу с приёмочными тестами. Без этого поля связь тоже есть, но косвенная: задача →
REQ → сценарии, которые на него ссылаются. «Статус: готово» агент ставит после зелёных тестов:
## T-003: Отмена неоплаченной записи через 48 часовТребования: REQ-005, REQ-008Зависит от: T-002Сценарии: SC-005Готово, когда: тест test_sc_005_… зелёный
## T-004: Отмена студентом и возвратТребования: REQ-006, REQ-007, REQ-008, REQ-011Зависит от: T-002Сценарии: SC-006, SC-007, SC-010, SC-011Готово, когда: тесты четырёх сценариев зелёные, остальные тесты не сломаныФайл остаётся в формате курса: задание sdd/task-graph из урока SDD.2.3 лишние поля просто не читает.
Хорошая задача умещается в один небольшой дифф и одну итерацию: десятки строк кода, а не сотни. T-003 и T-004 не
зависят друг от друга. Их можно отдать двум сессиям параллельно, но новичку проще идти последовательно. У Spec Kit
формат другой (- [ ] T004 [P] [US1] …, где [P] — «можно параллельно»), а смысл тот же.
Проверьте, что в задачах есть тесты. Шаблон задач Spec Kit по умолчанию прямо говорит «Tests are OPTIONAL»: задачи на тесты появятся, только если их потребовали конституция или спецификация (урок SDD.3.1). Поэтому принцип «тесты сначала» записан в конституции, а у каждой задачи есть сценарии — в поле «Сценарии» или через её требования.
Шаг 4. Реализация короткими итерациями
Заголовок раздела «Шаг 4. Реализация короткими итерациями»Одна задача — один круг
Заголовок раздела «Одна задача — один круг»Вот промпт на задачу. Он одинаковый для всех задач, меняется только номер. В «своём процессе» из урока SDD.3.3
его оформляют командой /spec-do, ниже — его развёрнутый вид:
Выполни задачу T-004 «Отмена студентом и возврат» из specs/001-enrollment/tasks.md.1. Перечитай требования и сценарии задачи в specs/001-enrollment/spec.md.2. Напиши тесты test_sc_006_…, test_sc_007_…, test_sc_010_…, test_sc_011_… в tests/acceptance/, запусти их и покажи, что они падают.3. Напиши минимальный код, чтобы они прошли. Файлы вне задачи не трогай.4. Запусти все тесты, поставь в задаче «Статус: готово» и покажи git diff --stat. Коммит сделаю я.Шаг 2 — это разработка через тестированиеразработка через тестирование (TDD)EN Test-Driven DevelopmentПрактика Кента Бека: сначала падающий тест, потом минимальный код, чтобы он прошёл, потом рефакторинг («красный — зелёный — рефакторинг»). Тесты становятся исполняемой спецификацией. в версии для агента. Тест, который упал до кода, доказывает, что он что-то проверяет. Как писать такие тесты из сценариев, разберём в уроке SDD.4.2.
Когда агент закончил, круг замыкает человек:
git diff --stat # 1. какие файлы изменились? неожиданный файл — первый вопросgit diff enrollment/ # 2. сам код: соответствует ли плану и требованиям задачиpython -m pytest -q # 3. тесты — своими руками, а не со слов агентаgit add -Agit commit -m "T-004: отмена студентом и возврат (REQ-006, REQ-007, REQ-008, REQ-011)"Коммит после каждой задачи — главная страховка процесса. Если следующая задача пойдёт не туда, откат к
последнему коммиту стоит секунды. Сообщение с ID задачи и требований продолжает трассируемость: команда
git log --grep REQ-006 покажет все изменения, связанные с правилом возврата.
Хуки: тесты запускает среда, а не память модели
Заголовок раздела «Хуки: тесты запускает среда, а не память модели»Правило «после правок запускай тесты» в CLAUDE.md агент иногда забывает. Особенно в конце длинной сессии,
когда контекст сжат. Хукхук агентаEN agent hookКоманда, которую среда агента запускает сама на определённом событии: после правки файла, перед действием, перед концом ответа. Работает независимо от памяти модели. В Claude Code хуки задают в .claude/settings.json; код выхода 2 блокирует действие и показывает агенту причину. — команда, которую запускает сама среда агента на
определённом событии. Ей не нужно, чтобы модель что-то помнила. В Claude Code хуки описывают в настройках проекта
.claude/settings.json, этот файл коммитят в репозиторий. Событие Stop наступает, когда агент заканчивает ответ:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "python \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run_tests.py" } ] } ] }}Сам хук — обычный скрипт. Claude Code передаёт ему данные события в stdin в формате JSON и смотрит на код
выхода: 0 — всё в порядке, 2 — «не останавливайся». Для события Stop код 2 не даёт агенту закончить ответ, а
текст из stderr уходит агенту как обратная связь:
"""Хук Stop: прежде чем агент закончит ответ, прогнать тесты."""import jsonimport subprocessimport sys
event = json.load(sys.stdin) # данные события в формате JSONif event.get("stop_hook_active"): # агент уже продолжает работу из-за этого хука — sys.exit(0) # второй раз не блокируем, чтобы не зациклиться
result = subprocess.run([sys.executable, "-m", "pytest", "-q", "-x", "--no-header"], capture_output=True, text=True)if result.returncode not in (0, 5): # 5 — pytest не нашёл тестов: не повод блокировать print("Тесты не проходят. Исправь код (не тесты!) по спецификации и запусти их снова:\n" + result.stdout[-1500:], file=sys.stderr) sys.exit(2)Проверить хук можно без агента: подайте ему JSON вручную. На исправном коде он молча выходит с кодом 0. Если же
испортить правило 7 суток (>= заменить на >), получится вот что:
$ echo '{"stop_hook_active": false}' | python .claude/hooks/run_tests.py; echo "exit=$?"Тесты не проходят. Исправь код (не тесты!) по спецификации и запусти их снова:.....F...FAILED tests/acceptance/test_enrollment.py::test_sc_006_full_refund_exactly_7_days1 failed, 5 passed in 0.04sexit=2Что важно знать о хуках:
- Поле
stop_hook_active. Без проверки этого поля упрямый красный тест зациклит агента. Claude Code и сам прерывает цепочку после нескольких блокировок подряд, но рассчитывать на это не стоит. - Другие события.
PostToolUseсрабатывает после каждой правки файла и подходит для форматирования и линтера.PreToolUseможет запретить действие: например, правку файлов вspecs/или вtests/acceptance/во время реализации. - Документация. Команда
/hooksпоказывает хуки проекта. Имена событий, поля JSON и коды выхода меняются от версии к версии — сверяйтесь с code.claude.com/docs/en/hooks-guide (документация Claude Code теперь живёт на code.claude.com; в старых статьях встречаются адреса на docs.anthropic.com и docs.claude.com). - Хук — это код с вашими правами. Он запускается автоматически, поэтому чужой
.claude/settings.jsonиз скачанного репозитория читайте так же внимательно, как скрипт установки.
Если у вашего агента хуков нет, тот же эффект дают обычные средства: хук git pre-commit, который не пускает
коммит с красными тестами, и CI на каждый push.
Субагенты: разведка и ревью в чистом контексте
Заголовок раздела «Субагенты: разведка и ревью в чистом контексте»СубагентсубагентEN subagent работает в своём окне контекста и возвращает только итог. В цикле SDD у него
две роли. Первая — разведка перед задачей: «найди все места, где считаются свободные места потока». Вторая —
независимое ревью после неё. Ревьюер не видел, как писался код, и сравнивает дифф только со спецификацией. В
Claude Code такого субагента описывают файлом в .claude/agents/:
---name: spec-reviewerdescription: Сверяет незакоммиченные изменения со спецификацией фичи. Вызывать после каждой задачи из tasks.md, до коммита.tools: Read, Glob, Grep, Bash---
Ты — ревьюер. Тебе называют задачу T-NNN и папку фичи specs/<фича>/.1. Прочитай требования и сценарии задачи в spec.md и правила specs/constitution.md.2. Посмотри git diff. Код не меняй.3. Верни список: какое требование не выполнено или выполнено не так (со ссылкой на REQ и строку кода); что сделано сверх задачи; какие имена не из docs/glossary.md; менялись ли тесты других задач.Если замечаний нет — так и напиши. Не хвали и не пересказывай дифф.Вызов — обычной фразой: «Попроси spec-reviewer проверить T-004». Поле tools оставляет ревьюеру только чтение и
терминал для git diff. Субагент не заменяет ваше ревью: он дешёво ловит механические расхождения, а смысл остаётся вам.
Цена — лишние токены и риск, что итог упустит деталь.
Что делает человек, пока агент работает
Заголовок раздела «Что делает человек, пока агент работает»«Кофе» в процессе есть, но он короткий и между контрольными точками. Вот как распределяется внимание:
| Когда | Что делает человек | Почему это нельзя отдать агенту |
|---|---|---|
| агент пишет спецификацию и план | готовит ответы экспертов, читает глоссарий | правила домена знает человек, а не модель |
| агент задаёт вопросы | отвечает или явно выносит вопрос за рамки | решение «пока вне рамок» тоже решение, и его принимает человек |
| агент реализует задачу | читает дифф предыдущей задачи, готовит следующую | ревью — главный вклад человека в качество |
| тесты зелёные | точечная проверка: граница руками, неожиданный файл в --stat | зелёные тесты проверяют только то, что в них написано |
| агент ошибся | записывает промах правилом в CLAUDE.md или конституцию | так память проекта учится на реальных ошибках |
| фича готова | проверка трассируемости, сведение спецификации и кода | агент склонен отчитываться «всё готово» |
Два приёма помогают не утонуть. Первый — ограничьте размер диффа: задача, после которой git diff --stat
показывает больше 200–300 строк, почти наверняка слишком крупная, разбейте её. Второй — не копите ревью: две
непрочитанные задачи подряд — сигнал остановиться. Три — и вы снова в вайб-кодинге, только с красивыми
markdown-файлами.
Как вернуть агента на рельсы
Заголовок раздела «Как вернуть агента на рельсы»Агент уходит от спецификации не из вредности. Пробел в контексте он заполняет самым типичным вариантом (урок SDD.1.2). Узнать уход можно по симптомам:
| Симптом | Что, скорее всего, случилось | Что делать |
|---|---|---|
| изменены файлы вне задачи | агент «заодно» улучшает код | откатить лишнее, напомнить «файлы вне задачи не трогай» |
| изменён тест, который раньше проходил | агент подгоняет тест под код | откатить тест; принцип 2 конституции; хук или запрет на правку тестов |
новые имена: Booking, refund_policy_manager | глоссарий выпал из контекста | указать на docs/glossary.md, прогнать линтер языка из урока DDD.1.2 |
| появилась «гибкость»: конфиг, база, абстракции | агент решает задачу шире, чем просили | сослаться на «Вне рамок» и принцип простоты |
правило реализовано «примерно» (> вместо >=) | граница не была в контексте или тест её не проверяет | указать на сценарий-границу; нет сценария — добавить в спецификацию |
| агент спорит со спецификацией или повторяет одну ошибку | контекст переполнен старыми попытками | новая сессия с одной задачей |
Инструменты — от лёгкого к тяжёлому:
-
Остановить. В Claude Code Esc прерывает ответ на полпути, сделанная работа сохраняется. Лучше остановить сразу, чем ждать, пока ошибка разрастётся.
-
Указать на конкретное место спецификации. Не «сделай нормально», а ссылка на требование и сценарий:
Стоп. Перечитай REQ-006 и SC-010 в specs/001-enrollment/spec.md. Что в твоём измененииим противоречит? Сначала ответь словами, код пока не трогай.Просьба сначала объяснить словами заставляет агента сверить код со спецификацией, а не латать симптом.
-
Откатить.
git restore <файл>илиgit reset --hardк последнему коммиту — и задачу заново. В Claude Code есть ещё/rewind(или Esc дважды) — откат правок и разговора к прошлой реплике. Но правки, сделанные командами в терминале, он не отслеживает, поэтому основная страховка — git. -
Новая сессия.
/clearи промпт задачи с нуля. Чистый контекст — это спецификация, план и одна задача без хвоста неудачных попыток. Именно поэтому всё важное должно жить в файлах, а не в переписке. -
Исправить спецификацию. Иногда агент «ошибся» потому, что ошиблась спецификация: правило неясное, сценария на границу нет. Тогда сначала правка
spec.md(со следующим свободным ID), потом код. Если поправить только код, спецификация и код разойдутся. Как ловить такое расхождение, разберём в уроке SDD.4.2.
Практика
Заголовок раздела «Практика»Подсказчик следующей задачи
Заголовок раздела «Подсказчик следующей задачи»Промпт задачи из шага 4 — шаблон, и его удобно собирать скриптом. Скрипт читает tasks.md, находит задачи, все
зависимости которых уже выполнены, и печатает промпт для первой из них. Сохраните его как tools/next_task.py:
"""Какие задачи из tasks.md можно брать сейчас — и готовый промпт для агента."""import reimport sysfrom pathlib import Path
TASK = re.compile(r"^## (T-\d{3}): (.+)$", re.M)
def field(body, name): """Значение строки «Имя: …» задачи или пустая строка, если такой строки нет.""" match = re.search(rf"^\**{name}:\**(.*)$", body, re.M) return match.group(1).strip() if match else ""
def parse_tasks(text): """Задачи по порядку. Обязательные поля формата курса — «Требования» и «Зависит от»; «Сценарии» и «Статус» — необязательные дополнения для работы с агентом.""" heads = list(TASK.finditer(text)) tasks = [] for i, head in enumerate(heads): body = text[head.end(): heads[i + 1].start() if i + 1 < len(heads) else len(text)] tasks.append({"id": head.group(1), "title": head.group(2), "requirements": re.findall(r"REQ-\d{3}", field(body, "Требования")), "deps": re.findall(r"T-\d{3}", field(body, "Зависит от")), "scenarios": re.findall(r"SC-\d{3}", field(body, "Сценарии")), "done_when": field(body, "Готово, когда"), "done": field(body, "Статус").lower() == "готово"}) return tasks
def ready(tasks): """Невыполненные задачи, у которых все зависимости уже выполнены.""" done = {t["id"] for t in tasks if t["done"]} return [t for t in tasks if not t["done"] and set(t["deps"]) <= done]
def prompt(task, feature): if task["scenarios"]: # поле «Сценарии» есть — называем тесты прямо tests = "тесты " + ", ".join(f"test_{sc.lower().replace('-', '_')}_…" for sc in task["scenarios"]) else: # нет — агент найдёт сценарии через требования: задача → REQ → SC tests = "тесты сценариев, которые ссылаются на " + ", ".join(task["requirements"]) return (f"Выполни задачу {task['id']} «{task['title']}» из specs/{feature}/tasks.md.\n" f"1. Перечитай {', '.join(task['requirements'])} и их сценарии в specs/{feature}/spec.md.\n" f"2. Напиши {tests} в tests/acceptance/, запусти и покажи, что они падают.\n" f"3. Напиши минимальный код, чтобы они прошли. Файлы вне задачи не трогай.\n" f"4. Готово, когда: {task['done_when'] or 'все тесты зелёные'}. Поставь «Статус: готово»\n" f" и покажи git diff --stat. Коммит сделаю я.")
if __name__ == "__main__": feature = sys.argv[1] if len(sys.argv) > 1 else "001-enrollment" tasks = parse_tasks(Path(f"specs/{feature}/tasks.md").read_text(encoding="utf-8")) candidates = ready(tasks) print(f"Сделано {sum(t['done'] for t in tasks)} из {len(tasks)}. " f"Можно брать: {', '.join(t['id'] for t in candidates) or 'нечего'}") if candidates: print("\n" + prompt(candidates[0], feature))В tasks.md из шага 3 добавьте задачи T-001 (деньги, статусы, исключения) и T-002 (запись на поток) со строкой
Статус: готово, и T-005 (проверка нагрузки, зависит от T-003 и T-004). Запуск из корня проекта:
python tools/next_task.py.
Сделано 2 из 5. Можно брать: T-003, T-004
Выполни задачу T-003 «Отмена неоплаченной записи через 48 часов» из specs/001-enrollment/tasks.md.1. Перечитай REQ-005, REQ-008 и их сценарии в specs/001-enrollment/spec.md.2. Напиши тесты test_sc_005_… в tests/acceptance/, запусти и покажи, что они падают.3. Напиши минимальный код, чтобы они прошли. Файлы вне задачи не трогай.4. Готово, когда: тест test_sc_005_… зелёный. Поставь «Статус: готово» и покажи git diff --stat. Коммит сделаю я.Уберите у T-003 строку «Сценарии» — скрипт не упадёт, а во втором пункте попросит «тесты сценариев, которые ссылаются на REQ-005, REQ-008». Так работает связь через требования. В уроке SDD.2.3 вы строили порядок всех задач топологической сортировкой. Здесь достаточно задач, готовых к работе прямо сейчас.
Практикум: пройдите цикл сами
Заголовок раздела «Практикум: пройдите цикл сами»- Создайте папку
school/сgit init, глоссарием из урока DDD.1.2, конституцией иCLAUDE.mdиз этого урока. - Перенесите спецификацию из урока SDD.2.1 в
specs/001-enrollment/spec.mdи проведите уточнения. - Получите
plan.mdв режиме планирования. Найдите решение, которое приняли бы иначе, и измените план до кода. - Получите
tasks.md, проверьте его скриптомnext_task.py, настройте хукStopи субагента-ревьюера. - Пройдите задачи по одной: промпт → красные тесты → код → ваше ревью → коммит. Записывайте, где агент ошибся и как вы это заметили, — пригодится в домашней работе.
Для сверки ниже — модель, которая получилась у автора урока после задач T-001…T-004. Ваша будет отличаться деталями, но правила те же.
Результат: enrollment/model.py после T-001…T-004 (откройте после своей попытки)
"""Контекст «Набор»: запись на поток, оплата за 48 часов, отмена с возвратом (specs/001-enrollment/)."""from dataclasses import dataclassfrom datetime import datetime, timedeltafrom enum import Enum
PAYMENT_PERIOD = timedelta(hours=48) # REQ-005: 48 часов с момента записиFULL_REFUND_PERIOD = timedelta(days=7) # REQ-006: 7 × 24 часа до старта потока
@dataclass(frozen=True)class Money: """Деньги: целое число копеек и валюта (REQ-009).""" amount: int currency: str
class EnrollmentStatus(Enum): PENDING_PAYMENT = "ожидает оплаты" CONFIRMED = "подтверждена" CANCELLED = "отменена"
class NoFreeSeats(Exception): """На потоке нет свободных мест."""
class AlreadyEnrolled(Exception): """У студента уже есть занимающая место запись на этот поток."""
class EnrollmentClosed(Exception): """Запись на поток закрыта: поток стартовал."""
class InvalidStatusTransition(Exception): """Переход статуса записи не разрешён правилами."""
@dataclassclass Enrollment: enrollment_id: str student_id: str enrolled_at: datetime status: EnrollmentStatus = EnrollmentStatus.PENDING_PAYMENT
@property def takes_seat(self): return self.status != EnrollmentStatus.CANCELLED
@dataclass(frozen=True)class EnrollmentCancelled: """ЗаписьОтменена (REQ-008).""" cohort_id: str enrollment_id: str student_id: str refund: Money occurred_at: datetime
class Cohort: """Поток курса — корень агрегата."""
def __init__(self, cohort_id, starts_at, capacity, price): self.cohort_id, self.starts_at, self.capacity, self.price = cohort_id, starts_at, capacity, price self._enrollments = {} self._events = []
@property def free_seats(self): return self.capacity - sum(e.takes_seat for e in self._enrollments.values())
def status_of(self, enrollment_id): return self._enrollments[enrollment_id].status
def enroll(self, student_id, now): """REQ-001…REQ-004.""" if now >= self.starts_at: raise EnrollmentClosed(f"Запись на поток {self.cohort_id} закрыта: поток стартовал") if any(e.student_id == student_id and e.takes_seat for e in self._enrollments.values()): raise AlreadyEnrolled(f"Студент {student_id} уже записан на поток {self.cohort_id}") if self.free_seats <= 0: raise NoFreeSeats(f"На потоке {self.cohort_id} нет свободных мест") enrollment_id = f"{self.cohort_id}-{len(self._enrollments) + 1}" self._enrollments[enrollment_id] = Enrollment(enrollment_id, student_id, enrolled_at=now) return enrollment_id
def confirm(self, enrollment_id, now): """Платёж по записи получен.""" enrollment = self._enrollments[enrollment_id] if enrollment.status != EnrollmentStatus.PENDING_PAYMENT: raise InvalidStatusTransition(f"Запись {enrollment_id} не ожидает оплаты") enrollment.status = EnrollmentStatus.CONFIRMED
def expire_unpaid(self, now): """REQ-005: отменить записи, которые 48 часов и дольше ожидают оплаты.""" for enrollment in self._enrollments.values(): if enrollment.status == EnrollmentStatus.PENDING_PAYMENT and now - enrollment.enrolled_at >= PAYMENT_PERIOD: self._cancel(enrollment, Money(0, self.price.currency), now)
def cancel(self, enrollment_id, now): """REQ-006, REQ-007, REQ-011: отмена студентом; возвращает сумму возврата.""" enrollment = self._enrollments[enrollment_id] if enrollment.status == EnrollmentStatus.CANCELLED: raise InvalidStatusTransition(f"Запись {enrollment_id} уже отменена") paid = enrollment.status == EnrollmentStatus.CONFIRMED if paid and self.starts_at - now >= FULL_REFUND_PERIOD: refund = self.price else: refund = Money(0, self.price.currency) self._cancel(enrollment, refund, now) return refund
def pull_events(self): events, self._events = self._events, [] return events
def _cancel(self, enrollment, refund, now): enrollment.status = EnrollmentStatus.CANCELLED self._events.append(EnrollmentCancelled(self.cohort_id, enrollment.enrollment_id, enrollment.student_id, refund, now))В enrollment/__init__.py — реэкспорт этих имён (from .model import …). Это учебная версия: без событий записи
и подтверждения, без защиты внутренностей агрегата. Полная модель с этими деталями — в уроке
DDD.3.3.
В домашней работе sdd-hw02-implement (её условие — в конце урока SDD.4.2) вы пройдёте этот цикл на другой фиче: спецификации «Выдачи сертификатов» уже готовы, а вести агента по задачам будете вы.
Проверь себя
Заголовок раздела «Проверь себя»- Полный цикл SDD: конституция один раз, затем спецификация → уточнения → план → задачи → реализация по задачам → проверка. Документы пишутся один раз на фичу, внутренний круг «задача → ревью → коммит» повторяется.
- Человек работает в контрольных точках: отвечает на вопросы, одобряет план, читает дифф каждой задачи. Кофе — между ними, а не вместо них. Непрочитанные задачи не копятся.
- Одна задача — тесты её сценариев сначала, минимальный код,
git diff --stat, коммит с ID задачи и требований. Коммит после каждой задачи делает откат дешёвым. - Хук запускает тесты силами среды, а не памяти модели. Субагент даёт разведку и независимое ревью в чистом контексте. Режим планирования переносит ошибки из кода в дешёвую реплику.
- Агента возвращают к спецификации ссылкой на конкретные REQ и SC, откатом, новой сессией. Если ошиблась сама спецификация — сначала правят её.
Тот же цикл «спецификация → план → задачи → код под тестами» для мультиагентного сервиса на LangGraph с FastAPI и двумя моделями в vLLM — в практикуме 8.5.5 основного курса.
Дальше: урок SDD.4.2 — проверка. Превратим сценарии в приёмочные тесты pytest, построим матрицу трассируемости, проверим сами тесты мутациями и научимся замечать, когда спецификация и код разошлись.