# Формат задач, целей и индексов Заголовок, мета-блок и строку индекса ставит `tasks.py add` — руками их не пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`; тело задачи (одну фразу, критерии, рамки, контекст) дописывает агент. ## Файл задачи `items/.md`: ```markdown # Тай-брейк при равной полноте - **Секция:** Ядро — вышла из спринта: остаток писал нерешённое в журнал - **Зачем:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт - **Теги:** goal:merge-robustness, kind:fix, sprint:2026-08-03 При столкновении точек выигрывает более полная, но при равной полноте побеждает последняя доставка — а она систематически беднее первой. ## Затрагивает Таблица `points` и её миграция; правило слияния в приёме доставки; формат отпечатка состояния на диске. Публичного контракта не трогает. ## Критерии приёмки - повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки - накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест - в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона ## Рамки Схема не трогается; данные только читаются; перезапуск сервиса допустим. Связано: решение о канонической форме содержимого. ``` - **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется префиксом `[goal]` / `[idea]`; обычная задача — без префикса. Отдельного поля типа **нет**: два места для одного факта разъезжаются, а префикс виден прямо в индексе, где и принимается решение «брать или не брать». - **Форма заголовка — по типу записи.** Задача отвечает на «что нужно сделать» и пишется глаголом в неопределённой форме («Печатать поле одним куском кода», «Не отбрасывать молча лишние символы»); цель — на «что приложение будет уметь»; идея просто называет, о чём она. Почему так — SKILL.md, «Как написана задача». `check` считает заголовки не в форме действия и печатает число в здоровье; годность формулировки смотрит агент `task-form`. - **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательна секция, причина после тире желательна (именно она объясняет, почему задача здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и теги опциональны. Порядок свободный, поле в одну строку. Нераспознанные поля сохраняются: скрипт правит свои и не трогает чужие. - **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль. Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в индексе: строка индекса его повторяет и производна от него, `check` сверяет, `check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле лежало только в индексе, штатная починка дрейфа теряла его молча и навсегда — а это единственное, по чему задачу выбирают, не открывая. - **Тело** — одна фраза «что станет наблюдаемо иначе», затрагиваемые границы, критерии приёмки, рамки, контекст, ссылки. Пишется на языке документации проекта: предметно, без англицизмов, у которых есть русское слово, и без терминов, которых нет ни в паспорте, ни в архитектуре, ни в конвенциях (правило и его причина — в SKILL.md, раздел «Как написана задача»). Мета **одной строкой через `·`** — прежняя форма. Она читается по-прежнему, `check` называет её дрейфом, `check --fix` переписывает списком; поле `Хук` при этом становится `Зачем`. Причина отказа от строки простая: с тремя полями и длинным «зачем» строка уезжала за экран, а `·` приходилось запрещать в тексте причины и самого «зачем». Тело — не план реализации и не спецификация: принятое и реализованное переезжает в документацию проекта, а файл задачи удаляется. ### Затрагивает Перечень **границ**, которых изменение касается. Границей считается то, у чего есть внешняя сторона и цена изменения: - эндпоинт, команда, форма ответа, код ответа; - таблица, поле, миграция, формат на диске, формат сообщения в очереди; - публичный тип или функция пакета, конфиг и его образцы; - внешний сервис или библиотека, чьё поведение становится нужным. Ничего из этого не трогается — так и пишется: «границ не трогает, изменение внутри одного узла». Это ответ, а не пустой раздел. **Границы, а не замысел.** «Переписать хранилище на новый драйвер» — замысел; `таблица points и её миграция`, `эндпоинт POST /ingest` — границы. Разница проверяется вопросом «это можно назвать до того, как решено *как* делать?»: если нет, строка описывает реализацию, и её место в предложении об изменении. **Свойства репозитория сюда не пишутся** — по той же причине, что и в рамки: имя таблицы стабильно, номер последней миграции протухает молча. Пишется `таблица points и её миграция`, а не `миграция 0042`. **Что из этого механизировано.** `check` и `sprint take` смотрят только на **наличие непустого раздела**. Полнота перечня машине не видна: границу, которую забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии: оценивать нечем. **У идей раздела нет** — как и критериев: границы становятся известны, когда идея превращается в задачу. ### Критерии приёмки 2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не «работает корректно», а «повторный прогон даёт тот же отпечаток — оракул: команда сверки». Это не второе определение готовности, а проектная конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже: там сказано «признак завершённости», здесь — «признак плюс чем проверяется». **Что из этого механизировано.** `check` и `sprint take` считают пункты: меньше двух — отказ («— работает» одной строкой больше не проходит), больше пяти — замечание, обычно это признак, что задача крупнее задачи. Наличие оракула проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид, что проверено больше проверенного, хуже, чем не проверять вовсе. **У идей критериев нет — именно поэтому они идеи.** **Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и возвращает задачу исполнителю**, а не держит невидимое сверх-требование. Иначе исполнитель никогда не знает, закончил ли, и мотивирован занижать критерии заранее. ### Рамки Одна строка: чего касаться нельзя, что перезапускается, что считается необратимым, трогается ли схема данных. **Свойства репозитория сюда не пишутся** — номер последней миграции, версия зависимости, хеш: в лежалой задаче они протухают молча и становятся ложной рамкой. Снимок берётся при постановке, а не при заведении. ### Вопросы Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом `question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет. **Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**, независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен отбору снаружи файла (`list --questions`, `list --tag question`), и его отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять. **Ответ на вопрос — три правки, и первая обязательна.** Раздел «Вопросы» опустошается: ответ переезжает в тело решением, а не остаётся вопросом рядом с ответом. Затем снимается тег (`edit --rm-tag question`) и переписывается «зачем»: «Решено: …» на вопрос «зачем нужна эта задача» уже не отвечает. **Порядок именно такой, потому что судит раздел, а не тег.** `sprint take` смотрит в непустой раздел и откажет взять задачу даже со снятым тегом, а `check` на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не опустошив раздел, — значит закольцевать себя между двумя советами. ## Файл цели **Заголовок цели отвечает на «что приложение будет уметь».** Не область работ и не имя подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от порядка доставки». Свойство поведения — тоже возможность. ```markdown # [goal] Исход слияния не зависит от порядка доставки - **Секция:** Направления - **Теги:** decomposed Ради чего: точки из разных доставок сходятся в один часовой объект, и сегодня исход столкновения зависит от порядка доставки, а не от содержания. ## Завершение - повторная доставка тех же точек в другом порядке даёт то же состояние; - накопительная метрика за сутки не уменьшается после повторной доставки; - в логе видно, какая из двух точек выиграла и почему. ``` - **Задачи цели здесь не перечисляются.** Перечень даёт `tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и поехал бы на первой же закрытой задаче. - **Раздел «Завершение» — списком, а не абзацем.** Это признаки того, что приложение уже умеет; **на строку «Завершения» ссылается задача**, объясняя, какую часть возможности она двигает (см. тест готовности). Абзацем такая ссылка не берётся, поэтому список. - **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет. Пометка именно **тегом**, а не строкой в теле: только так она проверяется. `check` напоминает о нём у цели без задач замечанием — неразобранная цель законна и зелёного прогона не ломает; `check --fix` сам ставит его цели, у которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть. - Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`. - **Достигнутая цель не исчезает.** `close <слаг> --implemented` удаляет файл и переносит строку в секцию `Готово` с датой: `- 2026-08-04 \`merge-order\` — Исход слияния не зависит от порядка доставки. …` Ссылки на файл в ней нет — файл удалён, а битая ссылка это ошибка `check`. Поведение живёт в спеках проекта; роадмап отвечает, **когда и в каком порядке** оно появилось. ## Слаг Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов (`foo-bar`, не `-foo`, `a--b`). Именуется **по сути, а не по текущей формулировке**: заголовок будет переписан при переоценке, а слаг стоит в ссылках из других задач, коммитов и черновиков. **Транслита не заводим** — `tie-break-equal-completeness`, а не `taj-brejk-pri-ravnoj-polnote`: транслит нечитаем для того, кто ищет по смыслу, и не сокращается. Переименование слага — не правка, а перенос ссылок: делается одним атомарным проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки, которых никто не проверяет. ## Индексы Строка везде одной формы: ```markdown - [Заголовок дословно](items/slug.md) — зачем ``` «Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние, остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке. | Файл | Что отвечает | Секции | | --- | --- | --- | | `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические и в этом порядке: `Запланировано`, `Направления`, `Сопровождение`, `Готово` (англ. `Planned`, `Directions`, `Operations`, `Done`) | | `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию Ядро/Инфра) | | `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» | | `REJECTED.md` | что ушло без реализации и почему | — | Шапку `SPRINT.md` пишет `sprint start` — **тем же мета-блоком, что у задачи**: поле на строку, `- **Цель:** [Заголовок](items/slug.md)`, `- **Начат:**` датой, `- **Спринт:**` слагом, которым метится урожай. Прежняя форма (три поля одной строкой через `·`) читается по-прежнему и уходит сама: файл переписывается на следующем `sprint start` и очищается на `sprint close`. Секции — **единственные заголовки `##` в индексе**: любой другой `##` в преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не имеет — порядка в беклоге нет вовсе. **Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до ответа человека, а следы остаются вопросами в файлах задач распущенного спринта. Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check` говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции **`Запланировано`** очередь значима и обосновывается прозой; двигают строку `move --section Запланировано --after <другой>`. В секции **`Готово`** строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в `REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда). **Секции роадмапа закреплены** — состав, полнота, единство языка и **порядок** проверяются `check`; секции беклога проект называет сам. Почему так — SKILL.md. Порядок закреплён потому, что `Готово` копится: стоя первым, достигнутое отодвигает за экран то, ради чего роадмап открывают чаще всего. **Заголовок секции пишется с прописной и отбивается пустой строкой с обеих сторон** — во всех индексах, включая секции беклога, имена которых выбирает проект. Написание канонических секций и отбивку правит `check --fix`; он же сводит написание секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**, файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру. Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`). Строку руками не пишут. Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё и складывает правки, и только потом пишет: сначала все временные файлы, потом переименования подряд. Полной транзакции на несколько файлов файловая система не даёт, но окно сжато до цепочки переименований, а **всё, что в нём может разъехаться, — производное**: файлы целы, индексы восстанавливает `check --fix`. Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при нетронутых индексах. `SPRINT.md` и есть артефакт заморозки: без него набор существует только в контексте сессии, и нарушение заморозки ненаблюдаемо. ## `REJECTED.md` Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет `tasks.py close --reason`, а `check` следит за форматом: ```markdown - 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла. Причина: калибровка болей — не боль, ни разу не возникло за полгода. Была секция: Инфра. ``` Реализованные сюда не попадают: у них остаётся коммит и документация. У выкинутой не остаётся ничего — и через квартал она возвращается тем же текстом. Это первое место, куда смотрит дедупликация при заведении. Запись не запрещает завести задачу заново: изменился контекст — заводим и ссылаемся на строку, объясняя, что изменилось. ## Теги Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по ним порцию разбора. Отдельных полей меты под это не заводим. - `goal:<слаг>` — цель, которой служит задача. Обязателен **у `kind:feature`**: новая возможность и есть содержание цели. У `fix`, `chore` и `research` его может не быть — они служат работоспособности, а не направлению, и в набор спринта входят помимо его цели. - `kind:<род>` — род работы: `feature` | `fix` | `chore` | `research`. Словарь **закрыт**, значение ровно одно. Обязателен у задачи (без него `sprint take` откажет), у цели запрещён, у идеи необязателен. Ставится `add --kind` / `edit --kind`; `--kind` заменяет прежнее значение, а не добавляет второе. Смысл рода и почему он тегом, а не префиксом — в SKILL.md, раздел «Род работы». - `question` — в файле есть неразобранный раздел «Вопросы». - `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит `sprint start` (по умолчанию — дата начала, он же пишется в `SPRINT.md`), и `add` при открытом спринте помечает заводимое. Тег, который надо помнить ставить руками, не ставится никогда — а на нём висит правило «первая порция разбора — урожай прошедшего спринта». - `decomposed` — на цели: разложена на задачи (см. «Файл цели»). Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка. Свои теги проект заводит свободно (партия ревью `review-ГГГГ-ММ-ДД`, тема, источник) — словарь не фиксирован. В индексы теги не выносим: индексы производны, отбор делает `list --tag`, а не глаза. ## Тест «готова к взятию» Задача готова, если из файла отвечаются четыре вопроса: 1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю, владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет ломаться Y при Z» — ответ. **У `kind:chore` адресат — разработчик, и это законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка. Род объявлен как раз затем, чтобы такие задачи не выдумывали себе пользовательскую пользу. 2. **Каких границ это касается** — раздел «Затрагивает». Без него задачу нельзя оценить: остаётся судить по длине текста. 3. **По чему видно, что закончено** — критерии приёмки с оракулами. 4. **Какую часть «Завершения» своей цели она двигает** — у задачи с целью. Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает тест не потому, что невидима снаружи, а потому, что не находит строки, к которой относится. Заодно видно обратное — достаточен ли набор задач для цели: строка «Завершения», к которой не относится ни одна задача, это незакрытая часть возможности. **У задачи без цели** (`fix`, `chore`, `research`) вопрос не задаётся: они служат работоспособности, а не направлению. Не отвечается первый, второй или третий вопрос → это **идея** (`[idea]`), её место в штурме. Не отвечается четвёртый у `feature` → либо цель есть и не проставлена, либо это не новая возможность. Отвечается всё, но задача не делается одним заходом и не мерджится целиком → это **несколько задач под одной целью**, дроби сразу. Промежуточного зонтика между целью и задачей нет: тип `[epic]` упразднён, потому что зонтиком стала сама цель. Тест применяется при заведении и при переоценке. К старым задачам, которых операция не касается, задним числом не применяется — беклог не переоформляют «заодно».