Перейти к содержимому
Программа SDDSDD.4 Практика и качество

Полный цикл с агентом: от спецификации к коду

Сквозной практикум на записи на потоки онлайн-школы: конституция, спецификация и уточнения, план в режиме планирования, задачи, реализация короткими итерациями с хуком на тесты, субагентом-ревьюером и коммитом после каждой задачи. Что делает человек, пока агент работает, и как возвращать агента к спецификации.

УРОКsdd/sdd-4-practice/01-workflow.mdx2 ч 30 минУрок 1 из 4
После урока вы сможете
  • Пройти с ИИ-агентом весь цикл SDD — конституция, спецификация, уточнения, план, задачи, реализация, проверка — и знать, что проверяет человек на каждом шаге
  • Вести реализацию короткими итерациями — одна задача, тесты сначала, ревью диффа, коммит — и автоматизировать прогон тестов хуком
  • Использовать режим планирования и субагентов там, где они экономят контекст и внимание
  • Замечать, что агент ушёл от спецификации, и возвращать его — остановкой, откатом, ссылкой на требование, новой сессией или правкой спецификации

Есть шутливое описание 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 ставишь ты после зелёных тестов.
- Нашёл противоречие или пробел в спецификации — остановись и спроси. Не додумывай.

Спецификация уже есть — это пример «запись на поток» из урока 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) и позволяет автоматически найти требования без задач и задачи без требований..

Режим планированиярежим планирования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). Но план в окне чата исчезнет вместе с сессией. Поэтому последняя фраза промпта и просит сохранить его в файл.

Из плана агент делает 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). Поэтому принцип «тесты сначала» записан в конституции, а у каждой задачи есть сценарии — в поле «Сценарии» или через её требования.

Вот промпт на задачу. Он одинаковый для всех задач, меняется только номер. В «своём процессе» из урока 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 -A
git 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 json
import subprocess
import sys
event = json.load(sys.stdin) # данные события в формате JSON
if 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_days
1 failed, 5 passed in 0.04s
exit=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-reviewer
description: Сверяет незакоммиченные изменения со спецификацией фичи. Вызывать после каждой задачи из 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
появилась «гибкость»: конфиг, база, абстракцииагент решает задачу шире, чем просилисослаться на «Вне рамок» и принцип простоты
правило реализовано «примерно» (> вместо >=)граница не была в контексте или тест её не проверяетуказать на сценарий-границу; нет сценария — добавить в спецификацию
агент спорит со спецификацией или повторяет одну ошибкуконтекст переполнен старыми попыткаминовая сессия с одной задачей

Инструменты — от лёгкого к тяжёлому:

  1. Остановить. В Claude Code Esc прерывает ответ на полпути, сделанная работа сохраняется. Лучше остановить сразу, чем ждать, пока ошибка разрастётся.

  2. Указать на конкретное место спецификации. Не «сделай нормально», а ссылка на требование и сценарий:

    Стоп. Перечитай REQ-006 и SC-010 в specs/001-enrollment/spec.md. Что в твоём изменении
    им противоречит? Сначала ответь словами, код пока не трогай.

    Просьба сначала объяснить словами заставляет агента сверить код со спецификацией, а не латать симптом.

  3. Откатить. git restore <файл> или git reset --hard к последнему коммиту — и задачу заново. В Claude Code есть ещё /rewind (или Esc дважды) — откат правок и разговора к прошлой реплике. Но правки, сделанные командами в терминале, он не отслеживает, поэтому основная страховка — git.

  4. Новая сессия. /clear и промпт задачи с нуля. Чистый контекст — это спецификация, план и одна задача без хвоста неудачных попыток. Именно поэтому всё важное должно жить в файлах, а не в переписке.

  5. Исправить спецификацию. Иногда агент «ошибся» потому, что ошиблась спецификация: правило неясное, сценария на границу нет. Тогда сначала правка spec.md (со следующим свободным ID), потом код. Если поправить только код, спецификация и код разойдутся. Как ловить такое расхождение, разберём в уроке SDD.4.2.

Промпт задачи из шага 4 — шаблон, и его удобно собирать скриптом. Скрипт читает tasks.md, находит задачи, все зависимости которых уже выполнены, и печатает промпт для первой из них. Сохраните его как tools/next_task.py:

"""Какие задачи из tasks.md можно брать сейчас — и готовый промпт для агента."""
import re
import sys
from 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 вы строили порядок всех задач топологической сортировкой. Здесь достаточно задач, готовых к работе прямо сейчас.

  1. Создайте папку school/ с git init, глоссарием из урока DDD.1.2, конституцией и CLAUDE.md из этого урока.
  2. Перенесите спецификацию из урока SDD.2.1 в specs/001-enrollment/spec.md и проведите уточнения.
  3. Получите plan.md в режиме планирования. Найдите решение, которое приняли бы иначе, и измените план до кода.
  4. Получите tasks.md, проверьте его скриптом next_task.py, настройте хук Stop и субагента-ревьюера.
  5. Пройдите задачи по одной: промпт → красные тесты → код → ваше ревью → коммит. Записывайте, где агент ошибся и как вы это заметили, — пригодится в домашней работе.

Для сверки ниже — модель, которая получилась у автора урока после задач T-001…T-004. Ваша будет отличаться деталями, но правила те же.

Результат: enrollment/model.py после T-001…T-004 (откройте после своей попытки)
"""Контекст «Набор»: запись на поток, оплата за 48 часов, отмена с возвратом (specs/001-enrollment/)."""
from dataclasses import dataclass
from datetime import datetime, timedelta
from 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):
"""Переход статуса записи не разрешён правилами."""
@dataclass
class 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) вы пройдёте этот цикл на другой фиче: спецификации «Выдачи сертификатов» уже готовы, а вести агента по задачам будете вы.

Проверь себя5 вопросов
1 Агент закончил задачу T-004 «Отмена студентом и возврат». Тесты зелёные. git diff --stat показывает изменения в enrollment/model.py, tests/acceptance/test_enrollment.py и docs/glossary.md. Что сделать в первую очередь?
2 Чем хук Stop с кодом выхода 2 надёжнее строки «всегда запускай тесты» в CLAUDE.md?
3 Что из перечисленного — хорошие причины для отдельной новой сессии агента (/clear)? Выберите все верные варианты.Можно выбрать несколько ответов
4 При уточнениях агент нашёл, что REQ-006 обещает полный возврат и неоплаченной записи. Вы добавили новое требование о повторной отмене. Какой у него будет ID, если в спецификации были REQ-001…REQ-010?
5 Агент реализовал отмену и написал self.starts_at - now > FULL_REFUND_PERIOD. Тест SC-006 (ровно за 7 суток) упал. Агент предлагает поменять в тесте 10:00 на 09:59. Как правильно поступить?
  • Полный цикл SDD: конституция один раз, затем спецификация → уточнения → план → задачи → реализация по задачам → проверка. Документы пишутся один раз на фичу, внутренний круг «задача → ревью → коммит» повторяется.
  • Человек работает в контрольных точках: отвечает на вопросы, одобряет план, читает дифф каждой задачи. Кофе — между ними, а не вместо них. Непрочитанные задачи не копятся.
  • Одна задача — тесты её сценариев сначала, минимальный код, git diff --stat, коммит с ID задачи и требований. Коммит после каждой задачи делает откат дешёвым.
  • Хук запускает тесты силами среды, а не памяти модели. Субагент даёт разведку и независимое ревью в чистом контексте. Режим планирования переносит ошибки из кода в дешёвую реплику.
  • Агента возвращают к спецификации ссылкой на конкретные REQ и SC, откатом, новой сессией. Если ошиблась сама спецификация — сначала правят её.

Тот же цикл «спецификация → план → задачи → код под тестами» для мультиагентного сервиса на LangGraph с FastAPI и двумя моделями в vLLM — в практикуме 8.5.5 основного курса.

Дальше: урок SDD.4.2 — проверка. Превратим сценарии в приёмочные тесты pytest, построим матрицу трассируемости, проверим сами тесты мутациями и научимся замечать, когда спецификация и код разошлись.