Два независимых сабагента на 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>
253 lines
20 KiB
Markdown
253 lines
20 KiB
Markdown
# Формат задач, целей и индексов
|
||
|
||
Заголовок, мета-строку и строку индекса ставит `tasks.py add` — руками их не
|
||
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`;
|
||
тело задачи (одну фразу, критерии, рамки, контекст) дописывает агент.
|
||
|
||
## Файл задачи
|
||
|
||
`items/<slug>.md`:
|
||
|
||
```markdown
|
||
# Тай-брейк при равной полноте
|
||
|
||
**Секция:** ядро — вышла из спринта: остаток писал нерешённое в журнал · **Хук:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт · **Теги:** goal:merge-robustness, sprint:2026-08-03
|
||
|
||
При столкновении точек выигрывает более полная, но при равной полноте побеждает
|
||
последняя доставка — а она систематически беднее первой.
|
||
|
||
## Критерии приёмки
|
||
|
||
- повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки
|
||
- накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест
|
||
- в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона
|
||
|
||
## Рамки
|
||
|
||
Схема не трогается; данные только читаются; перезапуск сервиса допустим.
|
||
|
||
Связано: решение о канонической форме содержимого.
|
||
```
|
||
|
||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
|
||
префиксом `[goal]` / `[idea]` / `[epic]`; обычная задача — без префикса.
|
||
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
|
||
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
|
||
- **Мета-строка** — первая непустая строка после заголовка. Обязательна секция,
|
||
причина после тире желательна (именно она объясняет, почему задача здесь
|
||
оказалась — в том числе «вышла из спринта: …»), хук и теги опциональны. Поля
|
||
разделяются ` · `, порядок свободный. `·` — служебный разделитель: в тексте
|
||
причины и хука его быть не должно.
|
||
- **Хук живёт здесь, а не только в индексе.** Строка индекса его повторяет и
|
||
производна от него: `check` сверяет, `check --fix` восстанавливает пропавшую
|
||
строку **вместе с хуком**. Пока хук лежал только в индексе, штатная починка
|
||
дрейфа теряла его молча и навсегда — а хук это единственное, по чему задачу
|
||
выбирают, не открывая.
|
||
- **Тело** — одна фраза «что станет наблюдаемо иначе», критерии приёмки, рамки,
|
||
контекст, ссылки. Пишется на языке документации проекта.
|
||
|
||
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
||
в документацию проекта, а файл задачи удаляется.
|
||
|
||
### Критерии приёмки
|
||
|
||
2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не
|
||
«работает корректно», а «повторный прогон даёт тот же отпечаток — оракул:
|
||
команда сверки». Это не второе определение готовности, а проектная
|
||
конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже:
|
||
там сказано «признак завершённости», здесь — «признак плюс чем проверяется».
|
||
|
||
**Что из этого механизировано.** `check` и `sprint take` считают пункты: меньше
|
||
двух — отказ («— работает» одной строкой больше не проходит), больше пяти —
|
||
замечание, обычно это признак, что задача крупнее задачи. Наличие оракула
|
||
проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только
|
||
замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид,
|
||
что проверено больше проверенного, хуже, чем не проверять вовсе.
|
||
|
||
**У идей критериев нет — именно поэтому они идеи.**
|
||
|
||
**Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик
|
||
видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и
|
||
возвращает задачу исполнителю**, а не держит невидимое сверх-требование. Иначе
|
||
исполнитель никогда не знает, закончил ли, и мотивирован занижать критерии
|
||
заранее.
|
||
|
||
### Рамки
|
||
|
||
Одна строка: чего касаться нельзя, что перезапускается, что считается
|
||
необратимым, трогается ли схема данных. **Свойства репозитория сюда не пишутся**
|
||
— номер последней миграции, версия зависимости, хеш: в лежалой задаче они
|
||
протухают молча и становятся ложной рамкой. Снимок берётся при постановке, а не
|
||
при заведении.
|
||
|
||
### Вопросы
|
||
|
||
Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом
|
||
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
|
||
|
||
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
|
||
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
|
||
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
|
||
отбору снаружи файла (`list --questions`, `list --tag question`), и его
|
||
отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже
|
||
отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять.
|
||
|
||
**Ответ на вопрос — три правки, и первая обязательна.** Раздел «Вопросы»
|
||
опустошается: ответ переезжает в тело решением, а не остаётся вопросом рядом с
|
||
ответом. Затем снимается тег (`edit <slug> --rm-tag question`) и переписывается
|
||
хук: «Решено: …» на вопрос «почему это лежит в беклоге» уже не отвечает.
|
||
|
||
**Порядок именно такой, потому что судит раздел, а не тег.** `sprint take`
|
||
смотрит в непустой раздел и откажет взять задачу даже со снятым тегом, а `check`
|
||
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
|
||
опустошив раздел, — значит закольцевать себя между двумя советами.
|
||
|
||
## Файл цели
|
||
|
||
```markdown
|
||
# [goal] Прочность слияния
|
||
|
||
**Секция:** кусты · **Теги:** decomposed
|
||
|
||
Ради чего: точки из разных доставок сходятся в один часовой объект, и сегодня
|
||
исход столкновения зависит от порядка доставки, а не от содержания.
|
||
|
||
## Завершение
|
||
|
||
Достигнута, когда исход слияния не зависит ни от порядка, ни от времени
|
||
доставки, и это подтверждено повторным прогоном на живом корпусе.
|
||
```
|
||
|
||
- **Задачи цели здесь не перечисляются.** Перечень даёт
|
||
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
|
||
поехал бы на первой же закрытой задаче.
|
||
- **Раздел «Завершение»** — то, по чему видно, что цель достигнута.
|
||
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
||
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
||
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
||
`check` напоминает о нём у цели без задач замечанием — неразобранная цель
|
||
законна и зелёного прогона не ломает; `check --fix` сам ставит его цели, у
|
||
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
|
||
- Цель живёт в `PLAN.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
|
||
|
||
## Слаг
|
||
|
||
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
|
||
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути, а не по текущей
|
||
формулировке**: заголовок будет переписан при переоценке, а слаг стоит в ссылках
|
||
из других задач, коммитов и черновиков. **Транслита не заводим** —
|
||
`tie-break-equal-completeness`, а не `taj-brejk-pri-ravnoj-polnote`: транслит
|
||
нечитаем для того, кто ищет по смыслу, и не сокращается.
|
||
|
||
Переименование слага — не правка, а перенос ссылок: делается одним атомарным
|
||
проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки,
|
||
которых никто не проверяет.
|
||
|
||
## Индексы
|
||
|
||
Строка везде одной формы:
|
||
|
||
```markdown
|
||
- [Заголовок дословно](items/slug.md) — хук
|
||
```
|
||
|
||
Хук отвечает на «почему это лежит в беклоге» одним предложением: состояние,
|
||
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
||
|
||
| Файл | Что отвечает | Секции |
|
||
| --- | --- | --- |
|
||
| `PLAN.md` | какие есть цели, в каком порядке идёт линия и почему | линия (упорядоченная) и кусты |
|
||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию ядро/инфра) |
|
||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||
|
||
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
||
преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не
|
||
имеет — порядка в беклоге нет вовсе.
|
||
|
||
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
||
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
|
||
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
|
||
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
|
||
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В **линии** плана порядок значим и
|
||
обосновывается прозой; двигают строку `move <slug> --section линия --after
|
||
<другой>`.
|
||
|
||
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
||
Строку руками не пишут.
|
||
|
||
Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё
|
||
и складывает правки, и только потом пишет: сначала все временные файлы, потом
|
||
переименования подряд. Полной транзакции на несколько файлов файловая система не
|
||
даёт, но окно сжато до цепочки переименований, а **всё, что в нём может
|
||
разъехаться, — производное**: файлы целы, индексы восстанавливает `check --fix`.
|
||
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
|
||
нетронутых индексах.
|
||
|
||
`SPRINT.md` и есть артефакт заморозки: без него набор существует только в
|
||
контексте сессии, и нарушение заморозки ненаблюдаемо.
|
||
|
||
## `REJECTED.md`
|
||
|
||
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
|
||
`tasks.py close --reason`, а `check` следит за форматом:
|
||
|
||
```markdown
|
||
- 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла.
|
||
Причина: калибровка болей — не боль, ни разу не возникло за полгода.
|
||
Была секция: инфра.
|
||
```
|
||
|
||
Реализованные сюда не попадают: у них остаётся коммит и документация. У
|
||
выкинутой не остаётся ничего — и через квартал она возвращается тем же текстом.
|
||
Это первое место, куда смотрит дедупликация при заведении.
|
||
|
||
Запись не запрещает завести задачу заново: изменился контекст — заводим и
|
||
ссылаемся на строку, объясняя, что изменилось.
|
||
|
||
## Теги
|
||
|
||
Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по
|
||
ним порцию разбора. Отдельных полей мета-строки под это не заводим.
|
||
|
||
- `goal:<слаг>` — цель, которой служит задача. Обязателен: задача без цели не
|
||
попадёт ни в один спринт.
|
||
- `question` — в файле есть неразобранный раздел «Вопросы».
|
||
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
|
||
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
|
||
`sprint start` (по умолчанию — дата начала, он же пишется в `SPRINT.md`), и
|
||
`add` при открытом спринте помечает заводимое. Тег, который надо помнить
|
||
ставить руками, не ставится никогда — а на нём висит правило «первая порция
|
||
разбора — урожай прошедшего спринта».
|
||
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
|
||
|
||
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
|
||
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
|
||
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
|
||
|
||
Свои теги проект заводит свободно (партия ревью `review-ГГГГ-ММ-ДД`, тема,
|
||
источник) — словарь не фиксирован. В индексы теги не выносим: индексы
|
||
производны, отбор делает `list --tag`, а не глаза.
|
||
|
||
## Тест «готова к взятию»
|
||
|
||
Задача готова, если из файла отвечаются три вопроса:
|
||
|
||
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
||
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
||
ломаться Y при Z» — ответ.
|
||
2. **По чему видно, что закончено** — критерии приёмки с оракулами.
|
||
3. **Какой цели она служит** — тег `goal:` и одна строка «почему именно этой».
|
||
|
||
Не отвечается первый или второй вопрос → это **идея** (`[idea]`), её место в
|
||
штурме. Не отвечается третий → либо цель есть и не проставлена, либо задача не
|
||
служит ничему — тогда её не надо заводить.
|
||
|
||
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
|
||
**эпик** (`[epic]`), сперва декомпозиция. Эпик временен и исчезает после
|
||
разбора; цель (`[goal]`) постоянна — не путать.
|
||
|
||
Тест применяется при заведении и при переоценке. К старым задачам, которых
|
||
операция не касается, задним числом не применяется — беклог не переоформляют
|
||
«заодно».
|