Два независимых сабагента на av-dev-pm и av-dev-pipeline. Две находки нашли оба. Главная — моя же перестановка закрытия за коммит сломала reopen и батч. close печатал «дорога назад из git», а reopen искал коммит удаления, которого в новом порядке ещё нет: шаг 11 последний, учёт остаётся незакоммиченным. Проверено прогоном — отказ кодом 2 на свежезакрытой задаче. Тем же грязным деревом ломались rebase и worktree remove в батче: каждая закрывшая задачу ветка уехала бы в провалившиеся. Починено с обеих сторон: reopen берёт текст из HEAD, если коммита удаления нет, а шаг 11 коммитит учёт вторым коммитом. Вторая — канонический пример docs/.pm.json убивал tasks.py. Четыре документа показывали ключ tasks.sections, которого скрипт не знает: неизвестный ключ это код 3 на любой команде. Проект, заведённый по канону дословно, остался бы без работы с задачами, а docs.py при этом печатал «канон соблюдён». Секции живут в заголовках индекса и второго дома не получают. Остальные восемнадцать: init писал конфиг в упразднённый .tasks.json; looks_like_tasks не видел переименованный индекс; урожай спринта терял автотег после sprint close; ответ на вопрос по инструкции оставлял задачу незабираемой; adopt требовал недостижимого зелёного; путь отчёта триажа не переживал archive; review-specs не имел режима для стыка после слияния; три остатка «шаг 9а» несли предкоммитную позицию закрытия; sprint.md отрицал сам себя в пункте «Сделана». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
144 lines
12 KiB
Markdown
144 lines
12 KiB
Markdown
# Ведение спринта
|
||
|
||
Спринт — набор задач под одну цель, замороженный до его конца. Здесь то, что
|
||
происходит **внутри** спринта: как задача заканчивается, что считается сделанным,
|
||
кто принимает и что идёт в доклад. Как спринт набирается — шаг 4 в
|
||
[cadence.md](cadence.md).
|
||
|
||
## Наблюдаемые исходы задачи
|
||
|
||
Как они достигаются — дело пайплайна проекта. Сессия знает только исход и его
|
||
след.
|
||
|
||
- **Сделана** — по определению готовности ниже. `close <slug> --implemented`:
|
||
файл и строка удаляются, следом остаётся коммит. **Закрывает агент-оркестратор
|
||
последним шагом пайплайна, после коммита; приёмка человеком идёт позже и
|
||
отменяется `reopen`** — см. «Кто и когда закрывает».
|
||
- **Вышла из спринта** — `sprint drop <slug> --reason …`: возвращается в беклог
|
||
с вопросом в файле и **без живого незакоммиченного предложения** — иначе при
|
||
следующем взятии оно столкнётся с новым. Наработки, которые жалко терять,
|
||
переезжают в тело задачи текстом.
|
||
- **Переросла в эпик** — распознаётся **до того, как под неё заведено
|
||
предложение об изменении**, иначе его придётся выбрасывать. Помечается
|
||
`[epic]`, выходит из набора, уходит на декомпозицию; спринт продолжается
|
||
остальными, части в замороженный набор не добавляются.
|
||
- **Отменена решением по ходу** — `close <slug> --reason "<ссылка на решение>"`
|
||
прямо из спринта. Это редкий, но законный исход, и он называется в докладе.
|
||
|
||
**Конец спринта** — когда по каждой задаче набора наступил один из исходов. Не
|
||
«все сделаны»: иначе одна застрявшая задача держит спринт бесконечно. Затем
|
||
`sprint close`.
|
||
|
||
**Урожай заводится при закрытии спринта, а не при закрытии задачи.** Это
|
||
обязанность закрывающего: пройти по спискам находок от исполнителей и завести
|
||
недостающее интейком скилла `tasks` — с дедупликацией и картой человеку. Заводимое
|
||
метится тегом спринта само (`sprint:<слаг>`), поэтому первая порция следующей
|
||
сессии поднимается одной командой `list --tag sprint:<слаг>`. Спринт, закрытый
|
||
без этого шага, оставляет находки жить в отчётах — то есть нигде.
|
||
|
||
**Порядок здесь обязателен: урожай заводится ДО команды `sprint close`.**
|
||
Автотег ставится по слагу из `SPRINT.md`, а `sprint close` этот файл очищает;
|
||
заведённое после команды остаётся без тега и в первую порцию следующей сессии
|
||
не попадёт — молча, потому что пустой `list --tag` выглядит как «урожая не
|
||
было». Если так уже вышло, тег ставится руками: `add … --tag sprint:<слаг>`,
|
||
слаг берётся из отчёта `sprint close`.
|
||
|
||
**Провал спринта.** Сработал блокер — спринт распускается (`sprint close
|
||
--dissolve --reason …`), недоделанное возвращается в беклог, новый набор
|
||
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
||
замороженный набор, который нельзя двигать, только мешает.
|
||
|
||
## Определение готовности
|
||
|
||
Задача засчитывается сделанной, когда верно **всё**:
|
||
|
||
1. **Пайплайн задачи пройден до конца** — со своим определением готовности, за
|
||
которое отвечает проект: проверки, состав ревью, документация, коммит. Здесь
|
||
оно не пересказывается и не подменяется — **форма фиксирована, содержание
|
||
даёт `CLAUDE.md` проекта**. Пайплайна нет, задача сделана руками — условие
|
||
читается как «проверки проекта зелёные и изменение влито».
|
||
2. **Критерии приёмки проверены поимённо** — каждый со своим оракулом, исход по
|
||
каждому назван. Это единственное, что добавляет управление задачами: пайплайн
|
||
отвечает «сделано по правилам», критерии — «сделано то, что заказывали».
|
||
3. **Находки по ходу отданы списком** — исполнитель обязан их **назвать**
|
||
(каждую, с пометкой «заведена / не заведена: причина»), но **не обязан
|
||
заводить**: заведение интерактивно, оно требует дедупликации против беклога и
|
||
кладбища и решений человека. Обязанность **завести урожай** — на закрытии
|
||
спринта, ниже. Так автономный исполнитель не оказывается одновременно обязан
|
||
завести задачи и не вправе это сделать в одиночку.
|
||
|
||
### Кто и когда закрывает
|
||
|
||
**Задачу закрывает агент-оркестратор — тот же, кто её и сделал**, последним шагом
|
||
пайплайна, после коммита. Порядок:
|
||
|
||
1. пайплайн доводит задачу до коммита;
|
||
2. **после коммита** зовёт `Skill av-dev-pm:tasks` и закрывает задачу
|
||
(`close <slug> --implemented`); строка уходит из `SPRINT.md`;
|
||
3. **докладывает исход и по каждому критерию — оракул и наблюдаемый исход.**
|
||
Это доклад приёмщику, а не отметка «принято».
|
||
|
||
**Приёмщик и исполнитель здесь совпадают, и это принято сознательно** — цена
|
||
названа в `SKILL.md`, раздел «Стимулы». Поэтому закрытие **не окончательно**, а
|
||
доклад по критериям — не формальность: он единственное, по чему приёмка вообще
|
||
возможна.
|
||
|
||
**Порядок «коммит, потом закрытие» обязателен.** Закрытие удаляет файл задачи;
|
||
упавший коммит после закрытия оставил бы задачу закрытой без единого следа
|
||
работы.
|
||
|
||
**Дорога назад существует и обязана быть названа.** Человек на сессии сверил
|
||
критерии, и приёмка не сошлась — `tasks.py reopen <slug> --reason "приёмка не
|
||
сошлась: …"`:
|
||
файл восстанавливается из истории git, строка возвращается в набор идущего
|
||
спринта (или в беклог, если спринта нет), строка кладбища снимается. Тело
|
||
восстанавливается **на момент удаления** — всё, что было дописано позже, живёт
|
||
только в коммите задачи, и это называется в докладе.
|
||
|
||
### Кто и по чему принимает
|
||
|
||
Три условия, без которых пункт про критерии не исполняется никем:
|
||
|
||
1. **Критерии переживают файл задачи.** Файл удаляется при закрытии, поэтому
|
||
критерии копируются туда, где их увидит приёмщик. Куда именно — **отвечает
|
||
пайплайн проекта, а не слот в `CLAUDE.md`**: он переносит их в `tasks.md`
|
||
изменения на шаге заведения change. Проект без пайплайна называет своё место
|
||
сам.
|
||
2. **Принимает человек на сессии, а не отдельный агент.** Декорреляция
|
||
исполнителя и приёмщика в момент закрытия **снята** (решение о снятии и его
|
||
цена — в `SKILL.md`, «Стимулы»). Опоры остались три: сохранённый отчёт триажа
|
||
в `openspec/changes/archive/<id>/review/` (до архивации — `changes/<id>/review/`)
|
||
— независимый артефакт, `SPRINT.md` под git
|
||
и `reopen`. Переоценка на сессии и есть момент, когда критерии видит не
|
||
исполнитель.
|
||
3. **Расхождение — дефект критериев.** Приёмщик правит критерии и возвращает
|
||
задачу исполнителю **в этом же спринте**: ответ есть, остаток есть, по тесту
|
||
про остаток это не выход из спринта.
|
||
|
||
## Что врывается в замороженный набор
|
||
|
||
Только два класса — правило и его обоснование в SKILL.md. Здесь механика:
|
||
|
||
- вторжение **не добавляет** задачу в набор: `SPRINT.md` остаётся набором под
|
||
цель. Внеплановая работа делается и называется в докладе отдельной строкой
|
||
«внеплановое: что и почему»;
|
||
- если внеплановое требует больше пары часов, честнее распустить спринт, чем
|
||
делать вид, что набор соблюдается;
|
||
- всё остальное падает в беклог через обычный интейк и ждёт сессии.
|
||
|
||
## Доклад в конце спринта
|
||
|
||
Проверяемые якоря, а не пересказ:
|
||
|
||
- **Цель спринта** и по каждой задаче набора: **хеш коммита**, дословный исход
|
||
проверок проекта, **исход по каждому критерию приёмки**.
|
||
- **Какие развилки решались** и чем обоснованы.
|
||
- **Урожай:** сколько задач заведено, какие вопросы накопились, что вышло из
|
||
спринта и почему, что было внеплановым.
|
||
- **Поимённая сверка урожая** с отчётами ревью: каждая отложенная находка имеет
|
||
либо слаг, либо строку «не заведена: причина». Нулевой урожай при непустом
|
||
отчёте — сигнал, а не благополучие.
|
||
- **Созрела ли порция для сессии.** Решение звать — человека, напоминание —
|
||
обязанность агента: `⌈урожай / 8⌉` порций.
|
||
- **Границы покрытия** сжатой строкой: что в этом спринте не проверялось вовсе.
|