спринт без цели: цель стала необязательной, но не молчаливой
Цель была обязательной во всех трёх местах сразу: 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>
This commit is contained in:
@@ -2661,3 +2661,49 @@ JJJ): у профиля обязан быть один правильный от
|
|||||||
Читателей документа не перечисляет ни одна сторона — читатель назначается
|
Читателей документа не перечисляет ни одна сторона — читатель назначается
|
||||||
планом прогона. Прежняя ссылка на «таблицу читателей» пережила саму таблицу
|
планом прогона. Прежняя ссылка на «таблицу читателей» пережила саму таблицу
|
||||||
и обещала то, чего нет, — с той самой правки, которая таблицу и убрала.
|
и обещала то, чего нет, — с той самой правки, которая таблицу и убрала.
|
||||||
|
|
||||||
|
## 39. Спринт без цели — законный случай (2026-08-07)
|
||||||
|
|
||||||
|
Цель была обязательной: `sprint start --goal` требовал слаг, `check` считал
|
||||||
|
ошибкой набор без названной цели, `sprint take` отказывал задаче под чужой
|
||||||
|
целью. Модель описывала только спринт развития — а спринт бывает под багфикс,
|
||||||
|
под техдолг, под здоровье проекта. Такой набор собран **по работоспособности, а
|
||||||
|
не по направлению**, и цели у него нет не по недосмотру.
|
||||||
|
|
||||||
|
Обходной путь существовал и был хуже прямого: завести цель-пустышку («Здоровье
|
||||||
|
проекта») и вешать под неё `fix`-и. Тогда `ROADMAP.md` — документ про то, что
|
||||||
|
приложение умеет, — обрастает строками про то, что оно не ломается, а тег
|
||||||
|
`goal:` перестаёт значить направление.
|
||||||
|
|
||||||
|
**АЕААА. Цель у спринта необязательна, но её отсутствие — ответ, а не молчание.**
|
||||||
|
`sprint start` принимает `--goal <слаг>` **или** `--no-goal`, и голое отсутствие
|
||||||
|
обоих — отказ с объяснением. Причина в стимуле: цель называет человек, и это
|
||||||
|
единственный продуктовый вопрос всей сессии. Разреши мы заводить спринт просто
|
||||||
|
без флага — забытый флаг, лень спросить и осознанное решение стали бы неотличимы
|
||||||
|
на выходе, а дешевле всего из трёх агенту именно не спрашивать.
|
||||||
|
|
||||||
|
**АЕААБ. В спринте без цели цель не проверяется вовсе.** Набор берёт что угодно
|
||||||
|
готовое к взятию, включая задачи под разными целями: сверять не с чем. Правило
|
||||||
|
«набор служит одной цели» не ослаблено, оно просто не применяется — целей в
|
||||||
|
таком наборе не больше одной, их ноль. Взамен машинной проверки остаётся показ
|
||||||
|
набора человеку до заморозки: у бесцельного спринта это **единственная**
|
||||||
|
проверка состава, и в скилле это сказано прямо.
|
||||||
|
|
||||||
|
**АЕААВ. Признак «спринт идёт» — слаг, а не цель.** Прежде код спрашивал цель и
|
||||||
|
получал заодно ответ про то, открыт ли спринт; теперь эти вопросы разошлись.
|
||||||
|
Слаг подходит на роль признака лучше цели по существу: он есть у любого спринта,
|
||||||
|
потому что без него нечем проставить `sprint:<слаг>`, то есть нечем собрать
|
||||||
|
урожай. Поле «Цель» в шапке остаётся на месте и у бесцельного набора — пишется
|
||||||
|
прозой без ссылки: **«цели нет» и «цель потерялась» обязаны различаться**.
|
||||||
|
|
||||||
|
### Что из этого следует
|
||||||
|
|
||||||
|
145. **Необязательное поле, которое всё же решают, заводится парой «значение или
|
||||||
|
явный отказ».** Умолчанием тут был бы не выбор, а его отсутствие — и
|
||||||
|
отличить его от забывчивости уже не смог бы никто, включая автора.
|
||||||
|
146. **Признак «сущность существует» нельзя вешать на её необязательное поле.**
|
||||||
|
Пока цель была обязательной, `sprint_goal()` отвечал сразу на два вопроса,
|
||||||
|
и это работало ровно до тех пор, пока второй ответ не понадобился отдельно.
|
||||||
|
147. **Снятая проверка называет, что осталось вместо неё.** Цель не проверяется
|
||||||
|
— значит, за состав отвечают показ человеку и строка доклада; иначе
|
||||||
|
послабление читается как «здесь можно не думать».
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: session
|
name: session
|
||||||
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги, а также когда вернулись к проекту после перерыва и надо понять, где остановились. Формат и содержимое задач — скилл tasks."
|
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели (или решение, что спринт без цели) и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги, а также когда вернулись к проекту после перерыва и надо понять, где остановились. Формат и содержимое задач — скилл tasks."
|
||||||
---
|
---
|
||||||
|
|
||||||
# Сессия между спринтами
|
# Сессия между спринтами
|
||||||
|
|
||||||
Работа идёт спринтами: **набор задач под одну цель, замороженный до конца
|
Работа идёт спринтами: **набор задач, замороженный до конца спринта** — обычно
|
||||||
спринта**. Между спринтами — одна сессия из четырёх шагов. Этот скилл владеет
|
под одну цель, но бывает и без неё. Между спринтами — одна сессия из четырёх шагов. Этот скилл владеет
|
||||||
**ритуалом**: как сессия проводится и как спринт ведётся. Форматом и содержимым
|
**ритуалом**: как сессия проводится и как спринт ведётся. Форматом и содержимым
|
||||||
задач владеет скилл `tasks`, выполнением задачи — пайплайн проекта.
|
задач владеет скилл `tasks`, выполнением задачи — пайплайн проекта.
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ description: "Ритуал между спринтами и ведение са
|
|||||||
|
|
||||||
## Роли
|
## Роли
|
||||||
|
|
||||||
**Человек** выбирает цель спринта, разбирает вопросы, держит право на
|
**Человек** выбирает цель спринта — **или решает, что этот спринт без цели**, —
|
||||||
необратимое и на истину в самих данных.
|
разбирает вопросы, держит право на необратимое и на истину в самих данных.
|
||||||
|
|
||||||
**Агент — оркестрация.** Он собирает набор под названную цель, ставит задачи,
|
**Агент — оркестрация.** Он собирает набор под названную цель, ставит задачи,
|
||||||
принимает отчёты и докладывает. Кто именно делает задачу — исполнитель, сабагент,
|
принимает отчёты и докладывает. Кто именно делает задачу — исполнитель, сабагент,
|
||||||
@@ -47,7 +47,10 @@ description: "Ритуал между спринтами и ведение са
|
|||||||
`question`.
|
`question`.
|
||||||
- **Блокер** — состояние, когда спринт не может продолжаться **ни одной**
|
- **Блокер** — состояние, когда спринт не может продолжаться **ни одной**
|
||||||
задачей.
|
задачей.
|
||||||
- **Спринт** — набор задач под одну цель, замороженный до его конца.
|
- **Спринт** — набор задач, замороженный до его конца. Под одной целью — или
|
||||||
|
**без цели вовсе**, законно: багфикс, техдолг, спринт здоровья. Такой набор
|
||||||
|
собран по работоспособности, а не по направлению, и заводится явно
|
||||||
|
(`sprint start --no-goal`).
|
||||||
|
|
||||||
## Вопрос, блокер, необратимое
|
## Вопрос, блокер, необратимое
|
||||||
|
|
||||||
@@ -88,9 +91,17 @@ description: "Ритуал между спринтами и ведение са
|
|||||||
|
|
||||||
## Заморозка набора
|
## Заморозка набора
|
||||||
|
|
||||||
**Цель одна.** Набор служит ей; задача, не служащая цели, в спринт не попадает,
|
**Целей не больше одной.** Названа цель — набор служит ей: задача под чужой
|
||||||
даже если взять удобно (`sprint take` это и запрещает). **Задача с открытым
|
целью в спринт не попадает, даже если взять удобно (`sprint take` это и
|
||||||
вопросом в набор не берётся.**
|
запрещает). **Задача с открытым вопросом в набор не берётся** — это верно всегда.
|
||||||
|
|
||||||
|
**Спринт без цели — законный случай, а не недосмотр.** Багфикс, техдолг,
|
||||||
|
здоровье: работа на работоспособность, а не на направление. Цель не названа —
|
||||||
|
сверять нечего, и в такой набор идёт что угодно готовое к взятию, в том числе
|
||||||
|
задачи под разными целями. Заводится он **явно**, `sprint start --no-goal`:
|
||||||
|
забытый флаг и решение человека иначе неотличимы, а это решение продуктовое.
|
||||||
|
Взамен проверки цели остаётся доклад — спринт без цели **называется таковым и
|
||||||
|
объясняется** одной строкой.
|
||||||
|
|
||||||
**Новая работа падает в беклог, а не в идущий спринт.** Решение «врываться или
|
**Новая работа падает в беклог, а не в идущий спринт.** Решение «врываться или
|
||||||
отложить» принимается один раз правилом, а не заново каждый раз. Врывается
|
отложить» принимается один раз правилом, а не заново каждый раз. Врывается
|
||||||
@@ -122,8 +133,8 @@ description: "Ритуал между спринтами и ведение са
|
|||||||
судьи документов канона на весь канон разом, раз в спринт: `doc-consistency`
|
судьи документов канона на весь канон разом, раз в спринт: `doc-consistency`
|
||||||
(документы между собой) и `doc-code-drift` (документы против кода).
|
(документы между собой) и `doc-code-drift` (документы против кода).
|
||||||
3. **Переоценка задач** порциями.
|
3. **Переоценка задач** порциями.
|
||||||
4. **Выбор цели и набор спринта.** Цель называет человек, набор собирает агент и
|
4. **Выбор цели и набор спринта.** Цель называет человек — либо называет, что
|
||||||
показывает **до старта работ**.
|
этот спринт без цели; набор собирает агент и показывает **до старта работ**.
|
||||||
|
|
||||||
Рёбра подписаны тем, что ломается при их нарушении:
|
Рёбра подписаны тем, что ломается при их нарушении:
|
||||||
|
|
||||||
@@ -155,7 +166,7 @@ flowchart TD
|
|||||||
|
|
||||||
1. `tasks.py check` — блок здоровья скажет состояние спринта, число готовых к
|
1. `tasks.py check` — блок здоровья скажет состояние спринта, число готовых к
|
||||||
взятию и залежавшихся; при расхождении раскладки `--fix`.
|
взятию и залежавшихся; при расхождении раскладки `--fix`.
|
||||||
2. Прочитать `SPRINT.md`: цель, состав, дата начала.
|
2. Прочитать `SPRINT.md`: цель (или запись, что её нет), состав, дата начала.
|
||||||
3. **Развилка, и решает её человек.** Набор всё ещё твой — продолжай спринт, ни
|
3. **Развилка, и решает её человек.** Набор всё ещё твой — продолжай спринт, ни
|
||||||
сессии, ни переоценки не нужно, они между спринтами. Взялся перечитывать,
|
сессии, ни переоценки не нужно, они между спринтами. Взялся перечитывать,
|
||||||
зачем эти задачи собраны вместе, — набор протух:
|
зачем эти задачи собраны вместе, — набор протух:
|
||||||
@@ -183,6 +194,7 @@ python3 $tk list --dir D --tag sprint:<слаг> # шаг 3: урожай с
|
|||||||
python3 $tk list --dir D --stale # шаг 3: дальше по залежалости
|
python3 $tk list --dir D --stale # шаг 3: дальше по залежалости
|
||||||
python3 $tk list --dir D --goal <слаг> # шаг 4: кандидаты под названную цель
|
python3 $tk list --dir D --goal <слаг> # шаг 4: кандидаты под названную цель
|
||||||
python3 $tk sprint start --dir D --goal <слаг> # шаг 4: заводит и слаг спринта
|
python3 $tk sprint start --dir D --goal <слаг> # шаг 4: заводит и слаг спринта
|
||||||
|
python3 $tk sprint start --dir D --no-goal # шаг 4: набор без цели, явным флагом
|
||||||
python3 $tk sprint take --dir D <слаг> … # шаг 4: набор
|
python3 $tk sprint take --dir D <слаг> … # шаг 4: набор
|
||||||
python3 $tk sprint close --dir D # конец спринта; --dissolve при блокере
|
python3 $tk sprint close --dir D # конец спринта; --dissolve при блокере
|
||||||
python3 $tk reopen <слаг> --dir D --reason … # приёмка не сошлась после закрытия
|
python3 $tk reopen <слаг> --dir D --reason … # приёмка не сошлась после закрытия
|
||||||
@@ -239,6 +251,11 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
|||||||
- **Сжать задачу до остатка** и отчитаться «сделана». Защита ослаблена там же.
|
- **Сжать задачу до остатка** и отчитаться «сделана». Защита ослаблена там же.
|
||||||
Пол для остатка — польза, названная в «зачем»; проверяет его человек при приёмке,
|
Пол для остатка — польза, названная в «зачем»; проверяет его человек при приёмке,
|
||||||
и `reopen` — его инструмент.
|
и `reopen` — его инструмент.
|
||||||
|
- **Объявить спринт без цели**, чтобы не задавать человеку продуктовый вопрос:
|
||||||
|
набор без цели берёт что угодно, и собрать его можно молча. Защита: цели нет
|
||||||
|
— это **ответ человека, а не умолчание** (`--no-goal` спрашивается так же, как
|
||||||
|
цель), плюс строка доклада, называющая спринт бесцельным и объясняющая почему.
|
||||||
|
Два бесцельных спринта подряд — предмет разбора процесса, а не мелочь.
|
||||||
- **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка
|
- **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка
|
||||||
с **сохранённым независимым отчётом**, а не с прозой исполнителя. Каждая
|
с **сохранённым независимым отчётом**, а не с прозой исполнителя. Каждая
|
||||||
отложенная находка имеет либо слаг, либо строку «не заведена: причина».
|
отложенная находка имеет либо слаг, либо строку «не заведена: причина».
|
||||||
|
|||||||
@@ -225,19 +225,28 @@
|
|||||||
по каждой цели-кандидату — сколько под ней задач без открытых вопросов
|
по каждой цели-кандидату — сколько под ней задач без открытых вопросов
|
||||||
(`list --goal <слаг>`). Цель без готовых задач набором не станет: её сперва
|
(`list --goal <слаг>`). Цель без готовых задач набором не станет: её сперва
|
||||||
надо декомпозировать.
|
надо декомпозировать.
|
||||||
2. **Цель называет человек.** Это продуктовое решение, а не механика: агент
|
2. **Цель называет человек** — либо называет, что цели не будет. Это
|
||||||
предлагает и объясняет, но не выбирает.
|
продуктовое решение, а не механика: агент предлагает и объясняет, но не
|
||||||
3. **Набор собирает агент** — `sprint start --goal <слаг>`, затем `sprint take
|
выбирает. **Оба ответа законны**, и «без цели» — такой же ответ, как слаг:
|
||||||
…`. Скрипт не даст взять цель, задачу с чужой целью, с открытым вопросом, без
|
спринт бывает под багфикс, под техдолг, под здоровье проекта. Спрашивается он
|
||||||
типа и **без разделов, которых требует её тип** (у `fix` это в том числе
|
так же, как цель, и в отдельный вопрос не выносится: это один и тот же вопрос
|
||||||
`Воспроизведение`, у `research` — `Вопрос` и `Куда ляжет ответ`, и сырьё
|
«подо что набираем».
|
||||||
поэтому не берётся вовсе). Задача без цели (`fix`, `chore`, `research`)
|
3. **Набор собирает агент** — `sprint start --goal <слаг>` (или `sprint start
|
||||||
берётся свободно — операционная работа входит в набор помимо его цели.
|
--no-goal`), затем `sprint take …`. Скрипт не даст взять цель, задачу с чужой
|
||||||
|
целью, с открытым вопросом, без типа и **без разделов, которых требует её
|
||||||
|
тип** (у `fix` это в том числе `Воспроизведение`, у `research` — `Вопрос` и
|
||||||
|
`Куда ляжет ответ`, и сырьё поэтому не берётся вовсе). Задача без цели (`fix`,
|
||||||
|
`chore`, `research`) берётся свободно — операционная работа входит в набор
|
||||||
|
помимо его цели. **В спринте без цели чужой цели нет вовсе**: сверять не с
|
||||||
|
чем, берётся что угодно готовое, и единственной защитой остаётся показ набора
|
||||||
|
человеку.
|
||||||
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
|
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
|
||||||
заморозки: после него набор не двигается. **В показе называется состав по
|
заморозки: после него набор не двигается. **В показе называется состав по
|
||||||
типам** — три `fix` и ни одной `feature` под целью развития это разговор про
|
типам** — три `fix` и ни одной `feature` под целью развития это разговор про
|
||||||
цель, а не про набор, и увидеть его надо до заморозки, а не в докладе по
|
цель, а не про набор, и увидеть его надо до заморозки, а не в докладе по
|
||||||
итогам.
|
итогам. **У набора без цели показ — единственная проверка состава**: скрипту
|
||||||
|
там отказывать не по чему, и «что угодно готовое» превращается в осмысленный
|
||||||
|
набор только глазами человека.
|
||||||
|
|
||||||
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
|
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
|
||||||
«Затрагивает» показывает границы до того, как заведено предложение об
|
«Затрагивает» показывает границы до того, как заведено предложение об
|
||||||
@@ -261,7 +270,8 @@
|
|||||||
названного, что разошлось.
|
названного, что разошлось.
|
||||||
- Изменения списком: удалено как реализованное (со ссылками), ушло без
|
- Изменения списком: удалено как реализованное (со ссылками), ушло без
|
||||||
реализации (с причинами), понижено до сырья, слито, сменило тип или цель.
|
реализации (с причинами), понижено до сырья, слито, сменило тип или цель.
|
||||||
- Новый спринт: цель, набор со слагами, дата, состав по типам.
|
- Новый спринт: цель — **или строка «без цели» с объяснением, почему** (багфикс,
|
||||||
|
техдолг, здоровье), — набор со слагами, дата, состав по типам.
|
||||||
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
||||||
цели остались — иначе доклад читается как «беклог разобран».
|
цели остались — иначе доклад читается как «беклог разобран».
|
||||||
- `tasks.py check` после правок — результат строкой.
|
- `tasks.py check` после правок — результат строкой.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Ведение спринта
|
# Ведение спринта
|
||||||
|
|
||||||
Спринт — набор задач под одну цель, замороженный до его конца. Здесь то, что
|
Спринт — набор задач, замороженный до его конца: под одну цель или **без цели**
|
||||||
происходит **внутри** спринта: как задача заканчивается, что считается сделанным,
|
(багфикс, техдолг, здоровье — это законно, `sprint start --no-goal`). Здесь то,
|
||||||
|
что происходит **внутри** спринта: как задача заканчивается, что считается сделанным,
|
||||||
кто принимает и что идёт в доклад. Как спринт набирается — шаг 4 в
|
кто принимает и что идёт в доклад. Как спринт набирается — шаг 4 в
|
||||||
[cadence.md](cadence.md).
|
[cadence.md](cadence.md).
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
- **Оказалась крупнее задачи** — распознаётся **до того, как под неё заведено
|
- **Оказалась крупнее задачи** — распознаётся **до того, как под неё заведено
|
||||||
предложение об изменении**, иначе его придётся выбрасывать. Выходит из набора,
|
предложение об изменении**, иначе его придётся выбрасывать. Выходит из набора,
|
||||||
уходит на декомпозицию; спринт продолжается остальными, части заводятся под той
|
уходит на декомпозицию; спринт продолжается остальными, части заводятся под той
|
||||||
же целью и в замороженный набор не добавляются.
|
же целью (у спринта без цели — без неё) и в замороженный набор не добавляются.
|
||||||
- **Отменена решением по ходу** — `close <slug> --reason "<ссылка на решение>"`
|
- **Отменена решением по ходу** — `close <slug> --reason "<ссылка на решение>"`
|
||||||
прямо из спринта. Это редкий, но законный исход, и он называется в докладе.
|
прямо из спринта. Это редкий, но законный исход, и он называется в докладе.
|
||||||
|
|
||||||
@@ -170,9 +171,9 @@ flowchart TD
|
|||||||
|
|
||||||
Только два класса — правило и его обоснование в SKILL.md. Здесь механика:
|
Только два класса — правило и его обоснование в SKILL.md. Здесь механика:
|
||||||
|
|
||||||
- вторжение **не добавляет** задачу в набор: `SPRINT.md` остаётся набором под
|
- вторжение **не добавляет** задачу в набор: `SPRINT.md` остаётся тем набором,
|
||||||
цель. Внеплановая работа делается и называется в докладе отдельной строкой
|
который заморозили и показали. Внеплановая работа делается и называется в
|
||||||
«внеплановое: что и почему»;
|
докладе отдельной строкой «внеплановое: что и почему»;
|
||||||
- если внеплановое требует больше пары часов, честнее распустить спринт, чем
|
- если внеплановое требует больше пары часов, честнее распустить спринт, чем
|
||||||
делать вид, что набор соблюдается;
|
делать вид, что набор соблюдается;
|
||||||
- всё остальное падает в беклог через обычный интейк и ждёт сессии.
|
- всё остальное падает в беклог через обычный интейк и ждёт сессии.
|
||||||
@@ -181,8 +182,10 @@ flowchart TD
|
|||||||
|
|
||||||
Проверяемые якоря, а не пересказ:
|
Проверяемые якоря, а не пересказ:
|
||||||
|
|
||||||
- **Цель спринта** и по каждой задаче набора: **хеш коммита**, дословный исход
|
- **Цель спринта** — или строка «спринт без цели» с тем, чем он был (багфикс,
|
||||||
проверок проекта, **исход по каждому критерию приёмки**.
|
техдолг, здоровье): у бесцельного набора это единственное место, где состав
|
||||||
|
вообще объясняется. И по каждой задаче набора: **хеш коммита**, дословный
|
||||||
|
исход проверок проекта, **исход по каждому критерию приёмки**.
|
||||||
- **Какие развилки решались** и чем обоснованы.
|
- **Какие развилки решались** и чем обоснованы.
|
||||||
- **Урожай:** сколько задач заведено, какие вопросы накопились, что вышло из
|
- **Урожай:** сколько задач заведено, какие вопросы накопились, что вышло из
|
||||||
спринта и почему, что было внеплановым.
|
спринта и почему, что было внеплановым.
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ docs/tasks/
|
|||||||
items/ задачи и цели файлами, <slug>.md, слаги английские
|
items/ задачи и цели файлами, <slug>.md, слаги английские
|
||||||
ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет
|
ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет
|
||||||
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
||||||
SPRINT.md текущий спринт: цель, набор, дата
|
SPRINT.md текущий спринт: цель (или её отсутствие), набор, дата
|
||||||
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
|
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ python3 $tk move S --dir D --section S [--reason R] [--after S | --first]
|
|||||||
python3 $tk close S --dir D --reason R # в REJECTED.md + удалить (ушла без реализации)
|
python3 $tk close S --dir D --reason R # в REJECTED.md + удалить (ушла без реализации)
|
||||||
python3 $tk close S --dir D --implemented # просто удалить (реализована и закоммичена)
|
python3 $tk close S --dir D --implemented # просто удалить (реализована и закоммичена)
|
||||||
python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась
|
python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась
|
||||||
python3 $tk sprint start --goal S --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
|
python3 $tk sprint start (--goal S | --no-goal) --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
|
||||||
python3 $tk init --dir D [--sections …] [--items …] [--backlog …] …
|
python3 $tk init --dir D [--sections …] [--items …] [--backlog …] …
|
||||||
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
|
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -278,12 +278,16 @@
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические и в этом порядке: `Запланировано`, `Направления`, `Сопровождение`, `Готово` (англ. `Planned`, `Directions`, `Operations`, `Done`) |
|
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические и в этом порядке: `Запланировано`, `Направления`, `Сопровождение`, `Готово` (англ. `Planned`, `Directions`, `Operations`, `Done`) |
|
||||||
| `BACKLOG.md` | что **можно взять** — только задачи | категории проекта (по умолчанию Ядро/Инфра) |
|
| `BACKLOG.md` | что **можно взять** — только задачи | категории проекта (по умолчанию Ядро/Инфра) |
|
||||||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
| `SPRINT.md` | какая цель (или что её нет) и какой набор заморожен | одна: «Набор» |
|
||||||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||||||
|
|
||||||
Шапку `SPRINT.md` пишет `sprint start` — **тем же мета-блоком, что у задачи**:
|
Шапку `SPRINT.md` пишет `sprint start` — **тем же мета-блоком, что у задачи**:
|
||||||
поле на строку, `- **Цель:** [Заголовок](items/slug.md)`, `- **Начат:**` датой,
|
поле на строку, `- **Цель:** [Заголовок](items/slug.md)`, `- **Начат:**` датой,
|
||||||
`- **Спринт:**` слагом, которым метится урожай. Прежняя форма (три поля одной
|
`- **Спринт:**` слагом, которым метится урожай. У спринта без цели
|
||||||
|
(`sprint start --no-goal`) поле «Цель» остаётся на месте и пишется прозой без
|
||||||
|
ссылки — «не названа»: **«цели нет» и «цель потерялась» обязаны различаться**.
|
||||||
|
Поэтому и признак «спринт идёт» — слаг, а не цель: слаг есть у любого спринта,
|
||||||
|
без него нечем метить урожай. Прежняя форма (три поля одной
|
||||||
строкой через `·`) читается по-прежнему и уходит сама: файл переписывается на
|
строкой через `·`) читается по-прежнему и уходит сама: файл переписывается на
|
||||||
следующем `sprint start` и очищается на `sprint close`.
|
следующем `sprint start` и очищается на `sprint close`.
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
Преемник backlog.py. Разница по существу одна: **порядка нет, есть цель**.
|
Преемник backlog.py. Разница по существу одна: **порядка нет, есть цель**.
|
||||||
Приоритеты и секция-как-уровень заменены на цель (`goal:<слаг>` тегом) и на
|
Приоритеты и секция-как-уровень заменены на цель (`goal:<слаг>` тегом) и на
|
||||||
спринт — замороженный набор задач под одну цель. Индексов теперь четыре, и
|
спринт — замороженный набор задач, обычно под одну цель. Индексов теперь
|
||||||
задача живёт ровно в одном из них за раз.
|
четыре, и задача живёт ровно в одном из них за раз.
|
||||||
|
|
||||||
Раскладка. Путь каталога — `docs/tasks`, жёстко: это часть канона документов
|
Раскладка. Путь каталога — `docs/tasks`, жёстко: это часть канона документов
|
||||||
av-dev, и подгоняется под него проект. Имена внутри настраиваются через
|
av-dev, и подгоняется под него проект. Имена внутри настраиваются через
|
||||||
@@ -17,7 +17,7 @@ av-dev, и подгоняется под него проект. Имена вн
|
|||||||
Направления | Сопровождение | Готово (или Planned |
|
Направления | Сопровождение | Готово (или Planned |
|
||||||
Directions | Operations | Done — один язык на индекс)
|
Directions | Operations | Done — один язык на индекс)
|
||||||
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
||||||
SPRINT.md текущий спринт: цель, набор, дата, слаг
|
SPRINT.md текущий спринт: цель (или её отсутствие), набор, дата, слаг
|
||||||
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
|
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
|
||||||
|
|
||||||
Источник истины — файл задачи в items/. Индексы производны: расходятся —
|
Источник истины — файл задачи в items/. Индексы производны: расходятся —
|
||||||
@@ -68,7 +68,7 @@ goal | feature | fix | chore | research, по-английски, как и пр
|
|||||||
tasks.py move S --section S [--reason R] [--after S | --first] [--dir DIR]
|
tasks.py move S --section S [--reason R] [--after S | --first] [--dir DIR]
|
||||||
tasks.py close S (--reason R | --implemented) [--dir DIR]
|
tasks.py close S (--reason R | --implemented) [--dir DIR]
|
||||||
tasks.py reopen S [--reason R] [--dir DIR]
|
tasks.py reopen S [--reason R] [--dir DIR]
|
||||||
tasks.py sprint start --goal S [--date ГГГГ-ММ-ДД] [--slug S] [--dir DIR]
|
tasks.py sprint start (--goal S | --no-goal) [--date ГГГГ-ММ-ДД] [--slug S] [--dir DIR]
|
||||||
tasks.py sprint take S [S …] [--dir DIR]
|
tasks.py sprint take S [S …] [--dir DIR]
|
||||||
tasks.py sprint drop S [S …] --reason R [--dir DIR]
|
tasks.py sprint drop S [S …] --reason R [--dir DIR]
|
||||||
tasks.py sprint close [--dissolve --reason R] [--dir DIR]
|
tasks.py sprint close [--dissolve --reason R] [--dir DIR]
|
||||||
@@ -996,7 +996,11 @@ def questions_open(lay: Layout, task: dict) -> bool:
|
|||||||
# --- Спринт ---
|
# --- Спринт ---
|
||||||
|
|
||||||
def sprint_goal(lay: Layout) -> tuple[str, str]:
|
def sprint_goal(lay: Layout) -> tuple[str, str]:
|
||||||
"""Слаг и заголовок цели текущего спринта; ('', '') — спринта нет."""
|
"""Слаг и заголовок цели текущего спринта; ('', '') — цели нет.
|
||||||
|
|
||||||
|
Пустой ответ значит «цель не названа», а НЕ «спринта нет»: спринт бывает
|
||||||
|
без цели законно (`--no-goal`). Идёт ли спринт — отвечает `sprint_started`.
|
||||||
|
"""
|
||||||
for line in read_lines(lay.index("sprint")):
|
for line in read_lines(lay.index("sprint")):
|
||||||
if (m := GOAL_LINE.match(line.strip())):
|
if (m := GOAL_LINE.match(line.strip())):
|
||||||
return Path(m.group(2)).stem, m.group(1)
|
return Path(m.group(2)).stem, m.group(1)
|
||||||
@@ -1011,6 +1015,16 @@ def sprint_slug(lay: Layout) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def sprint_started(lay: Layout) -> bool:
|
||||||
|
"""Идёт ли спринт. Признак — слаг, а не цель: цели может не быть.
|
||||||
|
|
||||||
|
Слаг пишет `sprint start` и стирает `sprint close`, он есть у любого
|
||||||
|
спринта — без него не проставить `sprint:<слаг>`, то есть не собрать
|
||||||
|
урожай. Поэтому он и есть наблюдаемое «спринт открыт».
|
||||||
|
"""
|
||||||
|
return bool(sprint_slug(lay))
|
||||||
|
|
||||||
|
|
||||||
def sprint_header(lay: Layout, goal_slug: str, goal_title: str,
|
def sprint_header(lay: Layout, goal_slug: str, goal_title: str,
|
||||||
date: str, slug: str) -> list[str]:
|
date: str, slug: str) -> list[str]:
|
||||||
"""Шапка спринта — мета-блок той же формы, что у задачи: поле на строку.
|
"""Шапка спринта — мета-блок той же формы, что у задачи: поле на строку.
|
||||||
@@ -1019,10 +1033,15 @@ def sprint_header(lay: Layout, goal_slug: str, goal_title: str,
|
|||||||
— обе регулярки её берут, — но чинить её нечем и не нужно: `SPRINT.md`
|
— обе регулярки её берут, — но чинить её нечем и не нужно: `SPRINT.md`
|
||||||
переписывается целиком на `sprint start` и очищается на `sprint close`,
|
переписывается целиком на `sprint start` и очищается на `sprint close`,
|
||||||
так что старая шапка живёт не дольше идущего спринта.
|
так что старая шапка живёт не дольше идущего спринта.
|
||||||
|
|
||||||
|
Спринт без цели пишет ту же строку прозой, без ссылки: поле остаётся на
|
||||||
|
месте, читатель видит решение, а `GOAL_LINE` его не берёт — «цель не
|
||||||
|
названа» и «цель потерялась» этим и различаются.
|
||||||
"""
|
"""
|
||||||
link = f"{lay.cfg['items']}/{goal_slug}.md"
|
goal_line = (f"- **Цель:** [{goal_title}]({lay.cfg['items']}/{goal_slug}.md)"
|
||||||
|
if goal_slug else "- **Цель:** не названа — набор без цели")
|
||||||
return ["# Спринт", "",
|
return ["# Спринт", "",
|
||||||
f"- **Цель:** [{goal_title}]({link})",
|
goal_line,
|
||||||
f"- **Начат:** {date}",
|
f"- **Начат:** {date}",
|
||||||
f"- **Спринт:** `{slug}`", "",
|
f"- **Спринт:** `{slug}`", "",
|
||||||
f"Урожай спринта поднимается `tasks.py list --tag {SPRINT_TAG}{slug}`"
|
f"Урожай спринта поднимается `tasks.py list --tag {SPRINT_TAG}{slug}`"
|
||||||
@@ -1032,7 +1051,9 @@ def sprint_header(lay: Layout, goal_slug: str, goal_title: str,
|
|||||||
def empty_sprint(lay: Layout) -> str:
|
def empty_sprint(lay: Layout) -> str:
|
||||||
return ("# Спринт\n\n"
|
return ("# Спринт\n\n"
|
||||||
"Спринта нет. Цель называет человек, набор собирает агент:\n"
|
"Спринта нет. Цель называет человек, набор собирает агент:\n"
|
||||||
"`tasks.py sprint start --goal <слаг>`.\n\n"
|
"`tasks.py sprint start --goal <слаг>`. Набор без цели —"
|
||||||
|
" `sprint start --no-goal`\n(багфикс, техдолг, здоровье:"
|
||||||
|
" работа на работоспособность, а не на направление).\n\n"
|
||||||
f"## {lay.cfg['sprint_section']}\n")
|
f"## {lay.cfg['sprint_section']}\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -1215,6 +1236,8 @@ def check(lay: Layout, fix: bool = False) -> int:
|
|||||||
if task["type"] and task["type"] not in TAKEABLE:
|
if task["type"] and task["type"] not in TAKEABLE:
|
||||||
errors.append(f"{name}: в спринте тип «{task['type']}»"
|
errors.append(f"{name}: в спринте тип «{task['type']}»"
|
||||||
f" — цель не берут вовсе, её берут её задачи")
|
f" — цель не берут вовсе, её берут её задачи")
|
||||||
|
# Спринт без цели ничьей цели и не противоречит: проверять нечему,
|
||||||
|
# набор там собран по другому признаку (работоспособность).
|
||||||
if goal_of_sprint and task["goal"] and task["goal"] != goal_of_sprint:
|
if goal_of_sprint and task["goal"] and task["goal"] != goal_of_sprint:
|
||||||
errors.append(f"{name}: цель задачи «{task['goal']}» не цель спринта"
|
errors.append(f"{name}: цель задачи «{task['goal']}» не цель спринта"
|
||||||
f" «{goal_of_sprint}» — набор служит одной цели")
|
f" «{goal_of_sprint}» — набор служит одной цели")
|
||||||
@@ -1267,11 +1290,18 @@ def check(lay: Layout, fix: bool = False) -> int:
|
|||||||
if goal_of_sprint and goal_of_sprint + ".md" not in tasks:
|
if goal_of_sprint and goal_of_sprint + ".md" not in tasks:
|
||||||
errors.append(f"{label['sprint']}: цель «{goal_of_sprint}» не найдена"
|
errors.append(f"{label['sprint']}: цель «{goal_of_sprint}» не найдена"
|
||||||
f" в {lay.cfg['items']}/")
|
f" в {lay.cfg['items']}/")
|
||||||
if not goal_of_sprint and entries["sprint"]:
|
# Спринт без цели законен (`sprint start --no-goal`), и по цели о том, идёт
|
||||||
errors.append(f"{label['sprint']}: набор есть, а цель не названа")
|
# ли он, судить нельзя. Судим по слагу: он есть у любого спринта, потому
|
||||||
if goal_of_sprint and not sprint_slug(lay):
|
# что без него не проставить `sprint:<слаг>` и не собрать урожай.
|
||||||
errors.append(f"{label['sprint']}: у спринта нет слага (- **Спринт:** `…`) —"
|
if entries["sprint"] and not sprint_started(lay):
|
||||||
f" урожай не отобрать; перезапусти `sprint start`")
|
errors.append(f"{label['sprint']}: набор есть, а шапки спринта нет"
|
||||||
|
f" (- **Спринт:** `…`) — урожай не отобрать, а «спринт идёт»"
|
||||||
|
f" не отличить от «строки остались от прошлого»;"
|
||||||
|
f" перезапусти `sprint start`")
|
||||||
|
elif goal_of_sprint and not sprint_started(lay):
|
||||||
|
errors.append(f"{label['sprint']}: цель названа, а слага спринта нет"
|
||||||
|
f" (- **Спринт:** `…`) — урожай не отобрать;"
|
||||||
|
f" перезапусти `sprint start`")
|
||||||
|
|
||||||
rejected = lay.index("rejected")
|
rejected = lay.index("rejected")
|
||||||
if rejected.is_file():
|
if rejected.is_file():
|
||||||
@@ -1338,8 +1368,9 @@ def health(lay: Layout, tasks: dict, entries: dict, sections: dict) -> None:
|
|||||||
" ответа на вопрос"))
|
" ответа на вопрос"))
|
||||||
|
|
||||||
goal_slug, _ = sprint_goal(lay)
|
goal_slug, _ = sprint_goal(lay)
|
||||||
if goal_slug:
|
if sprint_started(lay):
|
||||||
print(f" спринт: цель «{goal_slug}», слаг «{sprint_slug(lay) or '—'}»,"
|
goal_part = f"цель «{goal_slug}»" if goal_slug else "без цели"
|
||||||
|
print(f" спринт: {goal_part}, слаг «{sprint_slug(lay)}»,"
|
||||||
f" задач {len(entries['sprint'])}")
|
f" задач {len(entries['sprint'])}")
|
||||||
else:
|
else:
|
||||||
print(" спринт: не начат")
|
print(" спринт: не начат")
|
||||||
@@ -2140,7 +2171,11 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int:
|
|||||||
goal_of_sprint, _ = sprint_goal(lay)
|
goal_of_sprint, _ = sprint_goal(lay)
|
||||||
target = home_index({"type": rtype})
|
target = home_index({"type": rtype})
|
||||||
section = tmp["section"]
|
section = tmp["section"]
|
||||||
if target == "backlog" and goal_of_sprint and tmp["goal"] == goal_of_sprint:
|
# Возврат идёт в набор идущего спринта — тем же тестом, что и `sprint take`:
|
||||||
|
# мешает только чужая цель. Спринт без цели не мешает ничем, и задача без
|
||||||
|
# цели не мешает спринту с целью. Спринта нет — возврат в беклог.
|
||||||
|
fits_sprint = not (goal_of_sprint and tmp["goal"] and tmp["goal"] != goal_of_sprint)
|
||||||
|
if target == "backlog" and sprint_started(lay) and fits_sprint:
|
||||||
target, section = "sprint", lay.cfg["sprint_section"]
|
target, section = "sprint", lay.cfg["sprint_section"]
|
||||||
lines = read_lines(lay.index(target))
|
lines = read_lines(lay.index(target))
|
||||||
# Строка достигнутого снимается ДО вставки и на том же списке: иначе вторая
|
# Строка достигнутого снимается ДО вставки и на том же списке: иначе вторая
|
||||||
@@ -2210,12 +2245,21 @@ def meta_updated_text(text: str, reason: str, rtype: str | None = None) -> str |
|
|||||||
# --- Спринт ---
|
# --- Спринт ---
|
||||||
|
|
||||||
def cmd_sprint_start(lay: Layout, a: argparse.Namespace) -> int:
|
def cmd_sprint_start(lay: Layout, a: argparse.Namespace) -> int:
|
||||||
if (err := bad_slug(a.goal)):
|
# Цель либо названа, либо явно не названа. Голое отсутствие `--goal` не
|
||||||
|
# проходит намеренно: забытый флаг и решение «этот спринт без цели» иначе
|
||||||
|
# неразличимы, а второе — продуктовое решение человека.
|
||||||
|
if not a.goal and not a.no_goal:
|
||||||
|
raise Usage("цель спринта: --goal <слаг> или явно --no-goal."
|
||||||
|
" Спринт без цели законен (багфикс, техдолг, здоровье),"
|
||||||
|
" но называется решением, а не пропуском флага")
|
||||||
|
if a.goal and (err := bad_slug(a.goal)):
|
||||||
raise Usage(err)
|
raise Usage(err)
|
||||||
entries, _ = parse_entries(read_lines(lay.index("sprint")))
|
entries, _ = parse_entries(read_lines(lay.index("sprint")))
|
||||||
if entries:
|
if entries:
|
||||||
raise Usage(f"в {lay.name('sprint')} ещё есть набор ({len(entries)}) —"
|
raise Usage(f"в {lay.name('sprint')} ещё есть набор ({len(entries)}) —"
|
||||||
f" закрой спринт: tasks.py sprint close")
|
f" закрой спринт: tasks.py sprint close")
|
||||||
|
goal_title = ""
|
||||||
|
if a.goal:
|
||||||
gpath = lay.items / f"{a.goal}.md"
|
gpath = lay.items / f"{a.goal}.md"
|
||||||
if not gpath.exists():
|
if not gpath.exists():
|
||||||
raise Usage(f"цели {a.goal}.md нет в {lay.cfg['items']}/")
|
raise Usage(f"цели {a.goal}.md нет в {lay.cfg['items']}/")
|
||||||
@@ -2223,6 +2267,7 @@ def cmd_sprint_start(lay: Layout, a: argparse.Namespace) -> int:
|
|||||||
if goal["type"] != GOAL:
|
if goal["type"] != GOAL:
|
||||||
raise Usage(f"{a.goal} не цель (тип «{goal['type'] or '—'}») —"
|
raise Usage(f"{a.goal} не цель (тип «{goal['type'] or '—'}») —"
|
||||||
f" спринт набирается под одну цель")
|
f" спринт набирается под одну цель")
|
||||||
|
goal_title = goal["title"]
|
||||||
date = a.date or datetime.date.today().isoformat()
|
date = a.date or datetime.date.today().isoformat()
|
||||||
if not DATE_RE.fullmatch(date):
|
if not DATE_RE.fullmatch(date):
|
||||||
raise Usage("дата в формате ГГГГ-ММ-ДД")
|
raise Usage("дата в формате ГГГГ-ММ-ДД")
|
||||||
@@ -2236,17 +2281,27 @@ def cmd_sprint_start(lay: Layout, a: argparse.Namespace) -> int:
|
|||||||
if any(f"{SPRINT_TAG}{slug}" in t["tags"] for t in tasks.values()):
|
if any(f"{SPRINT_TAG}{slug}" in t["tags"] for t in tasks.values()):
|
||||||
print(f" внимание: тег {SPRINT_TAG}{slug} уже стоит на задачах прошлого спринта"
|
print(f" внимание: тег {SPRINT_TAG}{slug} уже стоит на задачах прошлого спринта"
|
||||||
f" — урожаи склеятся; задай другой `--slug`")
|
f" — урожаи склеятся; задай другой `--slug`")
|
||||||
lines = [*sprint_header(lay, a.goal, goal["title"], date, slug),
|
lines = [*sprint_header(lay, a.goal or "", goal_title, date, slug),
|
||||||
f"## {lay.cfg['sprint_section']}", ""]
|
f"## {lay.cfg['sprint_section']}", ""]
|
||||||
plan = Plan()
|
plan = Plan()
|
||||||
plan.index(lay, "sprint", lines)
|
plan.index(lay, "sprint", lines)
|
||||||
plan.commit()
|
plan.commit()
|
||||||
ready = [n[:-3] for n, t in tasks.items()
|
ready = [n[:-3] for n, t in tasks.items()
|
||||||
if t["goal"] == a.goal and t["type"] in TAKEABLE
|
if t["type"] in TAKEABLE
|
||||||
|
and (t["goal"] == a.goal if a.goal
|
||||||
|
else not (t["type"] in NEEDS_GOAL and not t["goal"]))
|
||||||
and not questions_open(lay, t) and not schema_verdict(lay, t)[0]]
|
and not questions_open(lay, t) and not schema_verdict(lay, t)[0]]
|
||||||
|
if a.goal:
|
||||||
print(f"спринт начат: цель «{a.goal}», {date}, слаг «{slug}»")
|
print(f"спринт начат: цель «{a.goal}», {date}, слаг «{slug}»")
|
||||||
print(f" кандидатов под цель без открытых вопросов: {len(ready)}"
|
print(f" кандидатов под цель без открытых вопросов: {len(ready)}"
|
||||||
+ (f" ({', '.join(sorted(ready))})" if ready else ""))
|
+ (f" ({', '.join(sorted(ready))})" if ready else ""))
|
||||||
|
else:
|
||||||
|
print(f"спринт начат: без цели, {date}, слаг «{slug}»")
|
||||||
|
print(f" набор без цели: цель не проверяется, в него идёт что угодно"
|
||||||
|
f" из готового к взятию ({len(ready)})."
|
||||||
|
f" Перечень — `tasks.py list --index backlog`")
|
||||||
|
print(" докладу это не безразлично: спринт без цели называется таковым"
|
||||||
|
" и объясняется (багфикс, техдолг, здоровье)")
|
||||||
print(f" заводимое по ходу метится тегом {SPRINT_TAG}{slug} само — это урожай")
|
print(f" заводимое по ходу метится тегом {SPRINT_TAG}{slug} само — это урожай")
|
||||||
print(" набери: tasks.py sprint take <слаг> …; набор показывается человеку до старта")
|
print(" набери: tasks.py sprint take <слаг> …; набор показывается человеку до старта")
|
||||||
return EXIT_OK
|
return EXIT_OK
|
||||||
@@ -2254,8 +2309,10 @@ def cmd_sprint_start(lay: Layout, a: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
def cmd_sprint_take(lay: Layout, a: argparse.Namespace) -> int:
|
def cmd_sprint_take(lay: Layout, a: argparse.Namespace) -> int:
|
||||||
goal_slug, _ = sprint_goal(lay)
|
goal_slug, _ = sprint_goal(lay)
|
||||||
if not goal_slug:
|
# Начат ли спринт, судим по слагу, а не по цели: спринт без цели идёт так же.
|
||||||
raise Usage("спринт не начат: tasks.py sprint start --goal <слаг>")
|
if not sprint_started(lay):
|
||||||
|
raise Usage("спринт не начат:"
|
||||||
|
" tasks.py sprint start (--goal <слаг> | --no-goal)")
|
||||||
sprint_lines = read_lines(lay.index("sprint"))
|
sprint_lines = read_lines(lay.index("sprint"))
|
||||||
backlog_lines = read_lines(lay.index("backlog"))
|
backlog_lines = read_lines(lay.index("backlog"))
|
||||||
hi, section = find_section(sprint_lines, lay.cfg["sprint_section"])
|
hi, section = find_section(sprint_lines, lay.cfg["sprint_section"])
|
||||||
@@ -2277,7 +2334,10 @@ def cmd_sprint_take(lay: Layout, a: argparse.Namespace) -> int:
|
|||||||
if t["type"] not in TAKEABLE:
|
if t["type"] not in TAKEABLE:
|
||||||
raise Usage(f"{slug}: тип «{t['type']}» в спринт не берётся —"
|
raise Usage(f"{slug}: тип «{t['type']}» в спринт не берётся —"
|
||||||
f" цель не берут вовсе, берут её задачи")
|
f" цель не берут вовсе, берут её задачи")
|
||||||
if t["goal"] and t["goal"] != goal_slug:
|
# Цель сверяется, только когда она у спринта есть. Спринт без цели
|
||||||
|
# берёт что угодно готовое: он собран по работоспособности, и чужой
|
||||||
|
# цели там нет — не с чем расходиться.
|
||||||
|
if goal_slug and t["goal"] and t["goal"] != goal_slug:
|
||||||
raise Usage(f"{slug}: цель «{t['goal']}» не цель спринта «{goal_slug}» —"
|
raise Usage(f"{slug}: цель «{t['goal']}» не цель спринта «{goal_slug}» —"
|
||||||
f" набор служит одной цели, даже если взять удобно")
|
f" набор служит одной цели, даже если взять удобно")
|
||||||
if not t["goal"] and t["type"] in NEEDS_GOAL:
|
if not t["goal"] and t["type"] in NEEDS_GOAL:
|
||||||
@@ -2383,7 +2443,8 @@ def cmd_sprint_close(lay: Layout, a: argparse.Namespace) -> int:
|
|||||||
plan = Plan()
|
plan = Plan()
|
||||||
plan.file(lay.index("sprint"), empty_sprint(lay))
|
plan.file(lay.index("sprint"), empty_sprint(lay))
|
||||||
plan.commit()
|
plan.commit()
|
||||||
print(f"спринт закрыт (цель «{goal_slug or '—'}», слаг «{slug or '—'}»)"
|
print(f"спринт закрыт ({f'цель «{goal_slug}»' if goal_slug else 'без цели'},"
|
||||||
|
f" слаг «{slug or '—'}»)"
|
||||||
+ (", набор распущен" if a.dissolve and entries else ""))
|
+ (", набор распущен" if a.dissolve and entries else ""))
|
||||||
print(f" история наборов остаётся в git: `git log -p {lay.index('sprint')}`")
|
print(f" история наборов остаётся в git: `git log -p {lay.index('sprint')}`")
|
||||||
if slug:
|
if slug:
|
||||||
@@ -3315,8 +3376,15 @@ def main() -> int:
|
|||||||
|
|
||||||
p = sub.add_parser("sprint", help="операции спринта")
|
p = sub.add_parser("sprint", help="операции спринта")
|
||||||
ssub = p.add_subparsers(dest="sprint_command", required=True)
|
ssub = p.add_subparsers(dest="sprint_command", required=True)
|
||||||
s = ssub.add_parser("start", help="начать спринт под названную цель")
|
s = ssub.add_parser("start", help="начать спринт под названную цель или без цели")
|
||||||
s.add_argument("--goal", required=True)
|
# Группа взаимоисключающая, но не required: отказ за отсутствие обоих
|
||||||
|
# флагов даёт cmd_sprint_start — его текст объясняет, а argparse только
|
||||||
|
# называет флаги.
|
||||||
|
g = s.add_mutually_exclusive_group()
|
||||||
|
g.add_argument("--goal", help="слаг цели, под которую набирается спринт")
|
||||||
|
g.add_argument("--no-goal", dest="no_goal", action="store_true",
|
||||||
|
help="набор без цели: багфикс, техдолг, здоровье —"
|
||||||
|
" работа на работоспособность, а не на направление")
|
||||||
s.add_argument("--date")
|
s.add_argument("--date")
|
||||||
s.add_argument("--slug", help="слаг спринта; по умолчанию дата начала")
|
s.add_argument("--slug", help="слаг спринта; по умолчанию дата начала")
|
||||||
s.add_argument("--dir")
|
s.add_argument("--dir")
|
||||||
|
|||||||
Reference in New Issue
Block a user