Цель была обязательной во всех трёх местах сразу: sprint start требовал слаг, check считал ошибкой набор без названной цели, sprint take отказывал задаче под чужой целью. Модель описывала только спринт развития — а спринт бывает под багфикс, под техдолг, под здоровье проекта: такой набор собран по работоспособности, а не по направлению, и цели у него нет не по недосмотру. Обходной путь существовал и был хуже прямого: завести цель-пустышку «Здоровье проекта» и вешать под неё fix-ы. Тогда ROADMAP.md — документ про то, что приложение умеет, — обрастает строками про то, что оно не ломается, а тег goal: перестаёт значить направление. Теперь sprint start принимает --goal <слаг> ИЛИ --no-goal, и голое отсутствие обоих — отказ с объяснением. Причина в стимуле: цель называет человек, и это единственный продуктовый вопрос всей сессии. Разреши мы заводить спринт просто без флага — забытый флаг, лень спросить и осознанное решение стали бы неотличимы на выходе, а дешевле всего из трёх агенту именно не спрашивать. Тот же обход записан в «Стимулы» скилла session вместе с защитой. В спринте без цели цель не проверяется вовсе: набор берёт что угодно готовое к взятию, включая задачи под разными целями. Правило «набор служит одной цели» не ослаблено, оно просто не применяется — целей там не больше одной, их ноль. Взамен машинной проверки остаётся показ набора человеку до заморозки, и в cadence.md сказано прямо: у бесцельного спринта это единственная проверка состава. Доклад обязан называть спринт бесцельным и объяснять, чем он был. Признак «спринт идёт» разъехался с целью и переехал на слаг: слаг есть у любого спринта, потому что без него нечем проставить sprint:<слаг>, то есть нечем собрать урожай. На sprint_started() переведены sprint take, блок здоровья check и reopen. Поле «Цель» в шапке остаётся и у бесцельного набора — пишется прозой без ссылки: «цели нет» и «цель потерялась» обязаны различаться. Побочно выправлен reopen: он возвращает задачу в набор идущего спринта тем же тестом, что и sprint take (мешает только чужая цель). Прежде тест был уже — совпадение целей, — и задача без цели, реопенутая при спринте с целью, уезжала в беклог вопреки прозе скилла. Тема 39 в DECISIONS.md, следствия 145-147. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
200 lines
17 KiB
Markdown
200 lines
17 KiB
Markdown
# Ведение спринта
|
||
|
||
Спринт — набор задач, замороженный до его конца: под одну цель или **без цели**
|
||
(багфикс, техдолг, здоровье — это законно, `sprint start --no-goal`). Здесь то,
|
||
что происходит **внутри** спринта: как задача заканчивается, что считается сделанным,
|
||
кто принимает и что идёт в доклад. Как спринт набирается — шаг 4 в
|
||
[cadence.md](cadence.md).
|
||
|
||
## Наблюдаемые исходы задачи
|
||
|
||
Как они достигаются — дело пайплайна проекта. Сессия знает только исход и его
|
||
след.
|
||
|
||
- **Сделана** — по определению готовности ниже. `close <slug> --implemented`:
|
||
файл и строка удаляются, следом остаётся коммит. **Закрывает агент-оркестратор
|
||
последним шагом пайплайна, после коммита; приёмка человеком идёт позже и
|
||
отменяется `reopen`** — см. «Кто и когда закрывает».
|
||
- **Вышла из спринта** — `sprint drop <slug> --reason …`: возвращается в беклог
|
||
с вопросом в файле и **без живого незакоммиченного предложения** — иначе при
|
||
следующем взятии оно столкнётся с новым. Наработки, которые жалко терять,
|
||
переезжают в тело задачи текстом.
|
||
- **Оказалась крупнее задачи** — распознаётся **до того, как под неё заведено
|
||
предложение об изменении**, иначе его придётся выбрасывать. Выходит из набора,
|
||
уходит на декомпозицию; спринт продолжается остальными, части заводятся под той
|
||
же целью (у спринта без цели — без неё) и в замороженный набор не добавляются.
|
||
- **Отменена решением по ходу** — `close <slug> --reason "<ссылка на решение>"`
|
||
прямо из спринта. Это редкий, но законный исход, и он называется в докладе.
|
||
|
||
**Конец спринта** — когда по каждой задаче набора наступил один из исходов. Не
|
||
«все сделаны»: иначе одна застрявшая задача держит спринт бесконечно. Затем
|
||
`sprint close`.
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
take["sprint take — задача в наборе"]
|
||
done["сделана<br/>close --implemented"]
|
||
out["вышла<br/>sprint drop --reason"]
|
||
epic["крупнее задачи<br/>распознаётся до заведения change"]
|
||
cancel["отменена решением по ходу<br/>close --reason"]
|
||
all{"по каждой задаче набора<br/>наступил исход?"}
|
||
harvest["урожай заводится интейком tasks"]
|
||
close["sprint close"]
|
||
dissolve["sprint close --dissolve --reason<br/>недоделанное — в беклог"]
|
||
|
||
take --> done
|
||
take --> out
|
||
take --> epic
|
||
take --> cancel
|
||
done --> all
|
||
out --> all
|
||
epic --> all
|
||
cancel --> all
|
||
all -->|да| harvest
|
||
harvest -->|"тег sprint: ставится, пока SPRINT.md не очищен"| close
|
||
take -->|"продолжать нечем ни одной задачей — блокер"| dissolve
|
||
done -->|"приёмка не сошлась: reopen --reason"| take
|
||
```
|
||
|
||
Два ребра на схеме — те, где порядок обязателен и нарушается молча: **урожай до
|
||
`sprint close`** (после команды автотег уже не поставится) и **блокер в обход
|
||
исходов** (спринт распускается, а не ждёт).
|
||
|
||
Схема — **сводка**: определение готовности и правила приёмки ниже, и при
|
||
расхождении прав текст.
|
||
|
||
**Урожай заводится при закрытии спринта, а не при закрытии задачи.** Это
|
||
обязанность закрывающего: пройти по спискам находок от исполнителей и завести
|
||
недостающее интейком скилла `tasks` — с дедупликацией и картой человеку. Заводимое
|
||
метится тегом спринта само (`sprint:<слаг>`), поэтому первая порция следующей
|
||
сессии поднимается одной командой `list --tag sprint:<слаг>`. Спринт, закрытый
|
||
без этого шага, оставляет находки жить в отчётах — то есть нигде.
|
||
|
||
**Порядок здесь обязателен: урожай заводится ДО команды `sprint close`.**
|
||
Автотег ставится по слагу из `SPRINT.md`, а `sprint close` этот файл очищает;
|
||
заведённое после команды остаётся без тега и в первую порцию следующей сессии
|
||
не попадёт — молча, потому что пустой `list --tag` выглядит как «урожая не
|
||
было». Если так уже вышло, тег ставится руками: `add … --tag sprint:<слаг>`,
|
||
слаг берётся из отчёта `sprint close`.
|
||
|
||
**Провал спринта.** Сработал блокер — спринт распускается (`sprint close
|
||
--dissolve --reason …`), недоделанное возвращается в беклог, новый набор
|
||
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
||
замороженный набор, который нельзя двигать, только мешает.
|
||
|
||
**Протухший набор — второй законный повод роспуска.** Работа стояла, и человек
|
||
вернулся к спринту, состав которого уже не держит в голове. Тем же роспуском:
|
||
`sprint close --dissolve --reason "работа стояла с <когда>"`, недоделанное в
|
||
беклог, новый набор — после переоценки, а не поверх старого.
|
||
|
||
Порога в неделях нет и не будет: счётчик простоя пришлось бы вести руками, а
|
||
решает всё равно человек. Признак — не срок, а **что набор перестал быть твоим**:
|
||
взялся перечитывать, зачем эти задачи вместе, — он протух. Заморозка тут не
|
||
мешает, она запрещает *двигать* набор, а не распустить его целиком.
|
||
|
||
## Определение готовности
|
||
|
||
Задача засчитывается сделанной, когда верно **всё**:
|
||
|
||
1. **Пайплайн задачи пройден до конца** — со своим определением готовности, за
|
||
которое отвечает проект: проверки, состав ревью, документация, коммит. Здесь
|
||
оно не пересказывается и не подменяется — **форма фиксирована, содержание
|
||
даёт `CLAUDE.md` проекта**. Пайплайна нет, задача сделана руками — условие
|
||
читается как «проверки проекта зелёные и изменение влито».
|
||
2. **Критерии приёмки проверены поимённо** — каждый со своим оракулом, исход по
|
||
каждому назван. Это единственное, что добавляет управление задачами: пайплайн
|
||
отвечает «сделано по правилам», критерии — «сделано то, что заказывали».
|
||
3. **Находки по ходу отданы списком** — исполнитель обязан их **назвать**
|
||
(каждую, с пометкой «заведена / не заведена: причина»), но **не обязан
|
||
заводить**: заведение интерактивно — оно требует дедупликации против беклога
|
||
и кладбища, а ещё решений человека. Обязанность **завести урожай** — на
|
||
закрытии спринта, ниже. Иначе автономный исполнитель оказался бы разом и
|
||
обязан завести задачи, и не вправе сделать это в одиночку.
|
||
|
||
### Кто и когда закрывает
|
||
|
||
**Задачу закрывает агент-оркестратор — тот же, кто её и сделал**, последним шагом
|
||
пайплайна, после коммита. Порядок:
|
||
|
||
1. пайплайн доводит задачу до коммита;
|
||
2. **после коммита** зовёт `Skill av-dev-pm:tasks` и закрывает задачу
|
||
(`close <slug> --implemented`); строка уходит из `SPRINT.md`;
|
||
3. **докладывает исход и по каждому критерию — оракул и наблюдаемый исход.**
|
||
Это доклад приёмщику, а не отметка «принято».
|
||
|
||
**Приёмщик и исполнитель здесь совпадают, и это принято сознательно** — цена
|
||
названа в `SKILL.md`, раздел «Стимулы». Поэтому закрытие **не окончательно**, а
|
||
доклад по критериям — не формальность: он единственное, по чему приёмка вообще
|
||
возможна.
|
||
|
||
**Порядок «коммит, потом закрытие» обязателен.** Закрытие удаляет файл задачи;
|
||
упавший коммит после закрытия оставил бы задачу закрытой без единого следа
|
||
работы.
|
||
|
||
**Само закрытие тоже коммитится, отдельным коммитом.** Удаление файла задачи и
|
||
правка индекса — правки в рабочем дереве; пока они не в истории, `SPRINT.md`
|
||
ничего не показывает, а `reopen` восстанавливает текст из `HEAD` в узком окне:
|
||
его закроет первый посторонний коммит. Сообщение про учёт, а не про
|
||
работу: `закрыта задача <slug>`.
|
||
|
||
**Дорога назад существует и обязана быть названа.** Человек на сессии сверил
|
||
критерии, и приёмка не сошлась — `tasks.py reopen <slug> --reason "приёмка не
|
||
сошлась: …"`:
|
||
файл восстанавливается из истории git, строка возвращается в набор идущего
|
||
спринта (или в беклог, если спринта нет), строка кладбища снимается. Тело
|
||
восстанавливается **на момент удаления** — всё, что было дописано позже, живёт
|
||
только в коммите задачи, и это называется в докладе.
|
||
|
||
### Кто и по чему принимает
|
||
|
||
Три условия, без которых пункт про критерии не исполняется никем:
|
||
|
||
1. **Критерии переживают файл задачи.** Файл удаляется при закрытии, поэтому
|
||
критерии копируются туда, где их увидит приёмщик. Куда именно — **отвечает
|
||
пайплайн проекта, а не слот в `CLAUDE.md`**: он переносит их в `tasks.md`
|
||
изменения, когда заводит change. Проект без пайплайна называет своё место
|
||
сам.
|
||
2. **Принимает человек на сессии, а не отдельный агент.** Исполнитель и приёмщик
|
||
в момент закрытия **не разведены** (решение о снятии и его
|
||
цена — в `SKILL.md`, «Стимулы»). Опоры остались три: **сохранённый независимый
|
||
отчёт ревью** (при конвейере `av-dev-pipeline` — отчёт триажа в
|
||
`openspec/changes/archive/<id>/review/`, до архивации — `changes/<id>/review/`),
|
||
`SPRINT.md` под git и `reopen`. Переоценка на сессии и есть момент, когда
|
||
критерии видит не исполнитель. **Конвейера ревью в проекте нет — первой опоры
|
||
нет тоже**, и это называется строкой доклада, а не обходится молча
|
||
(`SKILL.md`, «Стимулы»).
|
||
3. **Расхождение — дефект критериев.** Приёмщик правит критерии и возвращает
|
||
задачу исполнителю **в этом же спринте**: ответ есть, остаток есть, по тесту
|
||
про остаток это не выход из спринта.
|
||
|
||
## Что врывается в замороженный набор
|
||
|
||
Только два класса — правило и его обоснование в SKILL.md. Здесь механика:
|
||
|
||
- вторжение **не добавляет** задачу в набор: `SPRINT.md` остаётся тем набором,
|
||
который заморозили и показали. Внеплановая работа делается и называется в
|
||
докладе отдельной строкой «внеплановое: что и почему»;
|
||
- если внеплановое требует больше пары часов, честнее распустить спринт, чем
|
||
делать вид, что набор соблюдается;
|
||
- всё остальное падает в беклог через обычный интейк и ждёт сессии.
|
||
|
||
## Доклад в конце спринта
|
||
|
||
Проверяемые якоря, а не пересказ:
|
||
|
||
- **Цель спринта** — или строка «спринт без цели» с тем, чем он был (багфикс,
|
||
техдолг, здоровье): у бесцельного набора это единственное место, где состав
|
||
вообще объясняется. И по каждой задаче набора: **хеш коммита**, дословный
|
||
исход проверок проекта, **исход по каждому критерию приёмки**.
|
||
- **Какие развилки решались** и чем обоснованы.
|
||
- **Урожай:** сколько задач заведено, какие вопросы накопились, что вышло из
|
||
спринта и почему, что было внеплановым.
|
||
- **Поимённая сверка урожая** с независимыми отчётами ревью: каждая отложенная
|
||
находка имеет либо слаг, либо строку «не заведена: причина». Нулевой урожай при
|
||
непустом отчёте — сигнал, а не благополучие. **Отчётов нет** (проект без
|
||
конвейера ревью) — сверять не с чем, и строка доклада говорит именно это, а не
|
||
«сверено».
|
||
- **Созрела ли порция для сессии.** Решение звать — человека, напоминание —
|
||
обязанность агента: `⌈урожай / 8⌉` порций.
|
||
- **Границы покрытия** сжатой строкой: что в этом спринте не проверялось вовсе.
|