Основной инструмент владельца отвечал на половину своего вопроса. Оценка идёт по поведению: что приложение уже может и чего ещё не может, — а close --implemented удалял у достигнутой цели и файл, и строку, так что роадмап по построению показывал только «что осталось». Свидетельство лежало в самом роадмапе healthlog: секция «Что уже пройдено» на двадцать строк прозы, руками, с припиской «Эти звенья целями не заведены: закрытая цель записи не оставляет». Теперь строка с датой переезжает в секцию достигнутого, файл удаляется по-прежнему. Вторым домом поведения это не делает: нормативное поведение живёт в openspec/specs, роадмап отвечает, когда и в каком порядке оно появилось. Ссылки на файл в строке нет — файла больше нет, форма как в REJECTED.md. Цель стала возможностью приложения, задача — шагом к ней: - заголовок цели отвечает на «что приложение будет уметь»; свойство поведения («сообщает о своём состоянии», «исход не зависит от порядка») — тоже возможность и переформулировки не требует; - «Завершение» — списком, а не абзацем: задача ссылается на его строку, и это новая защита от «отрефакторить X» вместо прежнего «наблюдаемо снаружи». Заодно видно обратное: строка, к которой не относится ни одна задача, — незакрытая часть возможности; - работа над инструментом и процессом на этот вопрос не отвечает и живёт в отдельной секции. Цель обязательна не у всякой задачи. Прежнее «иначе она не попадёт ни в один спринт» было угрозой, а не аргументом, и заставляло операционную работу выдумывать себе направление. Граница по роду: feature без цели не бывает, fix, chore и research живут без неё и входят в набор помимо цели спринта. Тип [epic] упразднён: зонтиком стала цель, а слишком крупный шаг дробится под ней. Ноль употреблений на 97 записей двух живых проектов. Секции роадмапа — умеет / строим / направления / станок, четыре вместо двух; имена приняты как временные и запаркованы (TODO 7). Имя секции достигнутого знает скрипт — docs/.pm.json, ключ tasks.achieved_section. reopen цели снимает строку достигнутого, круг проверен вживую. Всё дописано в версию 3 канона: она ещё нигде не выкачена. DECISIONS 19, YYY–ГГГ и следствия 78–81. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
29 KiB
Формат задач, целей и индексов
Заголовок, мета-блок и строку индекса ставит tasks.py add — руками их не
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет check;
тело задачи (одну фразу, критерии, рамки, контекст) дописывает агент.
Файл задачи
items/<slug>.md:
# Тай-брейк при равной полноте
- **Секция:** ядро — вышла из спринта: остаток писал нерешённое в журнал
- **Зачем:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт
- **Теги:** goal:merge-robustness, kind:fix, sprint:2026-08-03
При столкновении точек выигрывает более полная, но при равной полноте побеждает
последняя доставка — а она систематически беднее первой.
## Затрагивает
Таблица `points` и её миграция; правило слияния в приёме доставки; формат
отпечатка состояния на диске. Публичного контракта не трогает.
## Критерии приёмки
- повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки
- накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест
- в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона
## Рамки
Схема не трогается; данные только читаются; перезапуск сервиса допустим.
Связано: решение о канонической форме содержимого.
-
Заголовок H1 — он же заголовок строки в индексе, дословно. Тип кодируется префиксом
[goal]/[idea]; обычная задача — без префикса. Отдельного поля типа нет: два места для одного факта разъезжаются, а префикс виден прямо в индексе, где и принимается решение «брать или не брать». -
Мета-блок — список сразу после заголовка, поле на строку. Обязательна секция, причина после тире желательна (именно она объясняет, почему задача здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и теги опциональны. Порядок свободный, поле в одну строку. Нераспознанные поля сохраняются: скрипт правит свои и не трогает чужие.
-
«Зачем» отвечает на «зачем нужна эта задача» — состояние, остаток, боль. Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в индексе: строка индекса его повторяет и производна от него,
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 <slug> --rm-tag question) и переписывается
«зачем»: «Решено: …» на вопрос «зачем нужна эта задача» уже не отвечает.
Порядок именно такой, потому что судит раздел, а не тег. sprint take
смотрит в непустой раздел и откажет взять задачу даже со снятым тегом, а check
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
опустошив раздел, — значит закольцевать себя между двумя советами.
Файл цели
Заголовок цели отвечает на «что приложение будет уметь». Не область работ и не имя подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от порядка доставки». Свойство поведения — тоже возможность.
# [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: транслит
нечитаем для того, кто ищет по смыслу, и не сокращается.
Переименование слага — не правка, а перенос ссылок: делается одним атомарным проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки, которых никто не проверяет.
Индексы
Строка везде одной формы:
- [Заголовок дословно](items/slug.md) — зачем
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние, остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
| Файл | Что отвечает | Секции |
|---|---|---|
ROADMAP.md |
что приложение уже умеет и чего ещё не умеет | умеет (достигнутое), строим (очередь значима), направления (очереди нет), станок (не про приложение) |
BACKLOG.md |
что можно взять — только задачи | секции проекта (по умолчанию ядро/инфра) |
SPRINT.md |
какая цель и какой набор под неё | одна: «Набор» |
REJECTED.md |
что ушло без реализации и почему | — |
Шапку SPRINT.md пишет sprint start — тем же мета-блоком, что у задачи:
поле на строку, - **Цель:** [Заголовок](items/slug.md), - **Начат:** датой,
- **Спринт:** слагом, которым метится урожай. Прежняя форма (три поля одной
строкой через ·) читается по-прежнему и уходит сама: файл переписывается на
следующем sprint start и очищается на sprint close.
Секции — единственные заголовки ## в индексе: любой другой ## в
преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не
имеет — порядка в беклоге нет вовсе.
Секции «блокеры» среди них нет. Блокер — состояние, а не полка: он живёт до
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
бы правилу «эскалируем немедленно», поэтому init её не заводит, а check
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции
строим очередь значима и обосновывается прозой; двигают строку
move <slug> --section строим --after <другой>. В секции умеет строки
не той формы, что у прочих индексов: дата, слаг, заголовок — как в
REJECTED.md, и по той же причине (файла уже нет, ссылаться некуда). Имена
секций временные, см. SKILL.md.
Индексы производны: расходятся с файлом — правим индексы (check --fix).
Строку руками не пишут.
Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё
и складывает правки, и только потом пишет: сначала все временные файлы, потом
переименования подряд. Полной транзакции на несколько файлов файловая система не
даёт, но окно сжато до цепочки переименований, а всё, что в нём может
разъехаться, — производное: файлы целы, индексы восстанавливает check --fix.
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
нетронутых индексах.
SPRINT.md и есть артефакт заморозки: без него набор существует только в
контексте сессии, и нарушение заморозки ненаблюдаемо.
REJECTED.md
Туда уходит задача, покинувшая беклог без реализации. Строку пишет
tasks.py close --reason, а check следит за форматом:
- 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, а не глаза.
Тест «готова к взятию»
Задача готова, если из файла отвечаются четыре вопроса:
-
Что станет наблюдаемо иначе, когда она сделана — снаружи: пользователю, владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет ломаться Y при Z» — ответ. У
kind:choreадресат — разработчик, и это законно: «уедет последний вызов устаревшего API» — ответ, а не отговорка. Род объявлен как раз затем, чтобы такие задачи не выдумывали себе пользовательскую пользу. -
Каких границ это касается — раздел «Затрагивает». Без него задачу нельзя оценить: остаётся судить по длине текста.
-
По чему видно, что закончено — критерии приёмки с оракулами.
-
Какую часть «Завершения» своей цели она двигает — у задачи с целью. Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает тест не потому, что невидима снаружи, а потому, что не находит строки, к которой относится. Заодно видно обратное — достаточен ли набор задач для цели: строка «Завершения», к которой не относится ни одна задача, это незакрытая часть возможности.
У задачи без цели (
fix,chore,research) вопрос не задаётся: они служат работоспособности, а не направлению.
Не отвечается первый, второй или третий вопрос → это идея ([idea]), её
место в штурме. Не отвечается четвёртый у feature → либо цель есть и не
проставлена, либо это не новая возможность.
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
это несколько задач под одной целью, дроби сразу. Промежуточного зонтика
между целью и задачей нет: тип [epic] упразднён, потому что зонтиком стала
сама цель.
Тест применяется при заведении и при переоценке. К старым задачам, которых операция не касается, задним числом не применяется — беклог не переоформляют «заодно».