ревью двумя проходами: 20 находок, все починены
Два независимых сабагента на 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>
This commit is contained in:
@@ -759,3 +759,53 @@ pyrefly: в окружении нет ничего, кроме линтеров,
|
||||
43. **Проверка не входит ни в один гейт.** CI у репозитория нет, хука нет;
|
||||
запускается руками командой из README. Заводить хук ради двух скриптов,
|
||||
которые правятся раз в месяц, — плата ритуалом без выгоды.
|
||||
|
||||
## 10. Ревью готовых плагинов двумя проходами (2026-08-03)
|
||||
|
||||
### Что было
|
||||
|
||||
Два независимых сабагента `fable` — по одному на `av-dev-pm` и `av-dev-pipeline`.
|
||||
**20 находок, из них две найдены обоими независимо.** Прошлые три круга ревью
|
||||
шли по одному проходу на всё; два прохода с разными предметами дали и больший
|
||||
урожай, и перекрёстное подтверждение самого дорогого дефекта.
|
||||
|
||||
### Что оказалось сломано по существу
|
||||
|
||||
**JJ. Перестановка закрытия за коммит (решение из темы 8) сломала `reopen` и
|
||||
батч — и это нашли оба прохода.** `close --implemented` печатает «дорога назад:
|
||||
файл восстанавливается из git», а `reopen` искал **коммит удаления**, которого в
|
||||
новом порядке ещё нет: шаг 11 идёт последним, и учёт остаётся незакоммиченным.
|
||||
Проверено прогоном: `reopen` отказывал кодом 2 на свежезакрытой задаче — то есть
|
||||
в самом вероятном своём применении. Тем же грязным деревом ломался `task-batch`:
|
||||
`git rebase` и `git worktree remove` отказывают, и **каждая успешно закрывшая
|
||||
задачу ветка** уезжала бы в провалившиеся.
|
||||
|
||||
Починено с обеих сторон: `reopen` берёт текст из `HEAD`, если коммита удаления
|
||||
нет, а шаг 11 обязан **коммитить учёт вторым коммитом** — иначе закрытие не
|
||||
доезжает до основной ветки и опора «`SPRINT.md` под git» остаётся словами.
|
||||
|
||||
**KK. Канонический пример `docs/.pm.json` убивал `tasks.py`.** `canon.md`,
|
||||
`skeletons.md`, `tasks/SKILL.md` и `adopt.md` показывали ключ `tasks.sections`,
|
||||
которого скрипт не знает: `_validate_config` отвергает неизвестные ключи кодом 3
|
||||
на **любой** команде. Проект, заведённый по канону дословно, остался бы без
|
||||
работы с задачами целиком — а `docs.py check` при этом печатал «канон соблюдён»,
|
||||
потому что чужой код 3 уходит в «не проверялось». Секции живут в заголовках `##`
|
||||
индекса и второго дома не получают.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
44. **Класс находок тот же, что и в прошлые три круга: стыки.** Не новый код, а
|
||||
место, где один файл ссылается на другой. `sprint.md` в пункте «Сделана» всё
|
||||
ещё отсылал к порядку, который сам же тремя экранами ниже отменил; три
|
||||
остатка «шаг 9а» несли **предкоммитную** позицию закрытия; путь отчёта
|
||||
триажа не переживал `opsx:archive`, хотя по нему сверяют полноту ревью
|
||||
четверо.
|
||||
45. **Инструкция, которую нельзя выполнить, выглядит как выполненная.** Ответ на
|
||||
вопрос по документированной процедуре (снять тег) оставлял задачу
|
||||
незабираемой, потому что судит **раздел**, а не тег; `canon adopt` требовал
|
||||
гнать `docs.py check` «до отсутствия дрейфа», недостижимого без нарушения
|
||||
запрета сочинять цели; урожай спринта, заведённый после `sprint close`,
|
||||
терял автотег молча.
|
||||
46. **Два прохода по разным предметам дороже одного, но не вдвое.** Перекрытие
|
||||
оказалось ровно в одной находке из двадцати — той самой, что подтвердилась
|
||||
дважды. Практика остаётся: ревью на плагин, а не одно на репозиторий.
|
||||
|
||||
@@ -100,7 +100,8 @@ openspec/
|
||||
«плагин не работает».
|
||||
|
||||
**При установке в проект, где лежали проектные копии** скиллов и агентов
|
||||
(`.claude/skills/{task-pipeline,review-pipeline,task-batch}`,
|
||||
(`.claude/skills/` — голые `task-pipeline`, `review-pipeline`, `task-batch`
|
||||
и с префиксом проекта `<проект>-task-pipeline`,
|
||||
`.claude/agents/<проект>-review-*.md`) — снеси их. Две копии одного скилла
|
||||
расходятся, и побеждает та, что короче названа.
|
||||
|
||||
|
||||
@@ -81,6 +81,19 @@
|
||||
- [x] починены 27 находок ruff и 14 pyrefly; `os` из `tasks.py` ушёл (40, 41)
|
||||
- [x] раздел «Проверка скриптов» в `README.md`
|
||||
|
||||
### 1.8 Ревью двумя проходами (тема 10)
|
||||
|
||||
- [x] `reopen` берёт текст из `HEAD`, когда коммита удаления ещё нет (JJ)
|
||||
- [x] шаг 11 коммитит учёт вторым коммитом; батч проверяет чистоту дерева (JJ)
|
||||
- [x] фиктивный ключ `tasks.sections` убран из четырёх документов (KK)
|
||||
- [x] `init` пишет конфиг в `docs/.pm.json`; `looks_like_tasks` его читает
|
||||
- [x] урожай спринта заводится до `sprint close`, слаг — из его отчёта
|
||||
- [x] ответ на вопрос опустошает раздел «Вопросы» — во всех трёх местах
|
||||
- [x] путь отчёта триажа переживает архивацию (5 мест)
|
||||
- [x] `review-specs` получил режим 3 — стык после слияния
|
||||
- [x] остальные 12 находок: `sprint.md`, «9а», перечень проектных копий,
|
||||
параллельность в батче, триаж в финальной сверке, триггеры профиля, 8–12
|
||||
|
||||
## 2. healthlog — первая боевая проверка
|
||||
|
||||
- [ ] `canon adopt`; `docs/backlog/` → `docs/tasks/`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: review-specs
|
||||
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить. Работает в двух режимах: дизайн/спеки ДО кода и код против спек ПОСЛЕ apply. Только чтение."
|
||||
description: "Сверка изменения с дельта-спеками в обе стороны — spec→code (каждое требование реализовано и подтверждено тестом) и, что важнее, code→spec (поведение, которое код имеет, а спека не заказывала: тихие ветки, самодеятельные дефолты, проглоченные ошибки, отброшенные поля, ретраи «на всякий случай»). Плюс границы спеки — что она не определяет и что пришлось домыслить. Работает в трёх режимах: дизайн/спеки ДО кода, код против спек ПОСЛЕ apply и стык после слияния нескольких задач, когда change уже заархивированы. Только чтение."
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: opus
|
||||
color: cyan
|
||||
@@ -43,6 +43,12 @@ Development на OpenSpec). Оптика — требования, а не ст
|
||||
намерение, а спека нормирует. Расхождение между proposal и дельтой — само по себе
|
||||
находка.
|
||||
|
||||
**Исключение — режим 3 (ниже): живого change нет.** Тогда источник требований —
|
||||
**актуальные** `openspec/specs/<capability>/spec.md`, а дельты поднимаются из
|
||||
архива (`openspec/changes/archive/<id>/specs/`) как свидетельство о намерении
|
||||
каждой слитой задачи. Задание обязано назвать этот режим явно; не названо —
|
||||
работаешь по режиму 1 или 2 и говоришь в границах покрытия, что change не нашёл.
|
||||
|
||||
Дополнительно поднимаешь: `design.md` и `tasks.md` change, затронутые актуальные
|
||||
спеки, инварианты из `CLAUDE.md`. Если тема ещё не перенесена в спеки и живёт
|
||||
только в `docs/architecture.md` — источник истины там, и это фиксируется в
|
||||
@@ -120,6 +126,27 @@ Development на OpenSpec). Оптика — требования, а не ст
|
||||
скажи об этом прямо, с последствием. Такая находка всегда `Действие: развилка`:
|
||||
менять спеку — решение человека.
|
||||
|
||||
## Режим 3 — стык после слияния нескольких задач
|
||||
|
||||
Зовётся финальной сверкой `task-batch`: несколько задач влиты в основную ветку,
|
||||
их change **уже заархивированы**, живой дельта-спеки не существует. Предмет —
|
||||
**только то, что появилось от слияния**, а не capability целиком заново: каждая
|
||||
задача уже проверена в своём worktree, и повторение даст те же находки дороже.
|
||||
|
||||
Ищешь ровно три вещи:
|
||||
|
||||
- **отменённое требование** — одна задача его выполнила, соседняя незаметно
|
||||
сняла; в актуальной спеке требование есть, в интегрированном коде его больше
|
||||
нет;
|
||||
- **два описания одного поведения** — два архивных change по-разному нормировали
|
||||
одно и то же, и актуальная спека собрала из них противоречие;
|
||||
- **осиротевшее поведение** — код, пришедший от слияния (разрешение конфликта,
|
||||
правка при rebase), которого не заказывал ни один из change.
|
||||
|
||||
База — интегрированный дифф основной ветки против точки, с которой батч начался.
|
||||
В границах покрытия скажи прямо: **capability целиком в этом режиме не
|
||||
сверялась**, проверялись стыки.
|
||||
|
||||
## Чего этот проход принципиально не может поймать
|
||||
|
||||
- Качество формы решения: код может точно соответствовать спеке и быть плохим.
|
||||
|
||||
@@ -378,7 +378,7 @@ Recall обоих равен длине их источника — это и е
|
||||
поэтому игнорируется; та же находка на предложении стоит абзаца обсуждения.
|
||||
|
||||
`rubric` живёт **только** в этом профиле. Судить код по критерию, под который он
|
||||
писался, — корреляция по построению; те же 8–14 свойств уже лежат приёмочными
|
||||
писался, — корреляция по построению; те же 8–12 свойств уже лежат приёмочными
|
||||
критериями в `tasks.md`.
|
||||
|
||||
## Контракт находок
|
||||
@@ -409,10 +409,15 @@ Recall обоих равен длине их источника — это и е
|
||||
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
|
||||
([references/review-journal.md](references/review-journal.md)) — сразу, не
|
||||
ретроспективно: теряется именно причина непоймания.
|
||||
- **Отчёт триажа сохраняется вместе с изменением** (например, в
|
||||
`openspec/changes/<id>/review/`). Он единственное, по чему потом видно, что
|
||||
было найдено и что из этого не заведено: нулевой урожай при непустом отчёте
|
||||
виден сразу.
|
||||
- **Отчёт триажа сохраняется вместе с изменением** — `openspec/changes/<id>/review/`.
|
||||
Он единственное, по чему потом видно, что было найдено и что из этого не
|
||||
заведено: нулевой урожай при непустом отчёте виден сразу.
|
||||
**Вместе с изменением он и переезжает:** после `opsx:archive` его адрес —
|
||||
`openspec/changes/archive/<id>/review/`. Кто ищет отчёт после архивации (батч
|
||||
на финальной сверке, приёмщик на сессии), смотрит **оба** пути; «отчёта нет»
|
||||
объявляется, только когда пуст и архивный, иначе самый дорогой сценарий
|
||||
«состав ревью неизвестен, гоняем заново» срабатывает на каждой доведённой
|
||||
задаче.
|
||||
|
||||
## Честный предел
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
| измеренные числа **с провенансом**, поведение внешних систем на самом деле | `docs/research/` |
|
||||
| конвенции прозой и **что уже механизировано** правилом | `docs/conventions/` |
|
||||
| почему решено так, отвергнутые варианты | `docs/adr/` |
|
||||
| типовые узлы, типовые ложноположительные, вопросы к проходам, недоступно проверке | `docs/review.md`, раздел настройки |
|
||||
| типовые узлы, типовые ложноположительные, вопросы к проходам, **триггеры профиля**, недоступно проверке | `docs/review.md`, раздел настройки |
|
||||
| прецеденты: воспроизведённые дефекты с оракулом | `docs/review.md`, журнал |
|
||||
| нормативное поведение и дельты изменения | `openspec/specs/`, `openspec/changes/<id>/specs/` |
|
||||
|
||||
|
||||
@@ -46,9 +46,11 @@ description: Проводит несколько задач разом — пл
|
||||
- **Батч не владеет спринтом и целями.** Он сообщает исход по каждой задаче в тех
|
||||
же трёх словах, что и `task-pipeline`: сделана / не доведена / оказалась крупнее
|
||||
задачи.
|
||||
- **Задачи закрывает пайплайн внутри каждого сабагента**, шагом 9а, вызовом
|
||||
Skill `av-dev-pm:tasks`. Батч сам записей учёта не трогает: он не знает, чем
|
||||
кончилась приёмка, и дублировать закрытие ему незачем. Урожай ревью батч
|
||||
- **Задачи закрывает пайплайн внутри каждого сабагента**, шагом 11 — после
|
||||
коммита работы и **отдельным коммитом учёта**, вызовом Skill `av-dev-pm:tasks`.
|
||||
Батч сам записей учёта не трогает: он не знает, чем кончилась приёмка, и
|
||||
дублировать закрытие ему незачем. Но грязное дерево после сабагента — **его**
|
||||
проблема: на нём откажут и `rebase`, и `worktree remove` (см. шаг 6). Урожай ревью батч
|
||||
отдаёт списком, а задачи из него заводит тот, кто ведёт задачи проекта.
|
||||
|
||||
## Ключевое отличие от одиночного пайплайна
|
||||
@@ -183,7 +185,8 @@ fast-forward. Ветки после вливания удаляются.
|
||||
записаны и куда; изменённые файлы; добавлялся ли нумерованный артефакт и с
|
||||
каким номером; затронутые capability; состояние гейта; **перечень
|
||||
запущенных проходов ревью поимённо с исходом каждого**; **путь к
|
||||
сохранённому отчёту триажа** (`openspec/changes/<id>/review/`); шло ли ревью
|
||||
сохранённому отчёту триажа** (`openspec/changes/<id>/review/`, после
|
||||
архивации — `openspec/changes/archive/<id>/review/`); шло ли ревью
|
||||
инлайн; границы покрытия.
|
||||
|
||||
Сабагент, упершийся в вопрос, **не останавливает батч**: он записывает вопрос,
|
||||
@@ -204,7 +207,8 @@ fast-forward. Ветки после вливания удаляются.
|
||||
не с чем; это само по себе основание не вливать, пока сабагент не назовёт
|
||||
профиль и не обоснует его по факту изменения.
|
||||
2. **Сверяй с независимым артефактом, а не с прозой отчёта.** Перечень проходов
|
||||
бери из **сохранённого отчёта триажа** (`openspec/changes/<id>/review/`) —
|
||||
бери из **сохранённого отчёта триажа** (`openspec/changes/<id>/review/` или
|
||||
`openspec/changes/archive/<id>/review/` — задача доведена, change заархивирован) —
|
||||
пайплайн обязан его туда положить. Проза сабагента написана тем же, кто мог
|
||||
проход и пропустить: она подтверждает сама себя. Отчёта триажа на месте нет —
|
||||
считай, что состав неизвестен, и дозапускай ревью целиком.
|
||||
@@ -250,6 +254,11 @@ rebase делается **внутри worktree задачи**, а ff-слиян
|
||||
розданы заранее). Неавтоматический конфликт — **не форсируй**: прерви
|
||||
(`git -C <path> rebase --abort`), оставь ветку и worktree как есть, вынеси это
|
||||
в доклад как нераспознанное пересечение;
|
||||
- **дерево сабагента обязано быть чистым.** `git -C <path> status --porcelain`
|
||||
до `rebase`: непусто — значит сабагент не довёл шаг 11 до коммита учёта (или
|
||||
оставил мусор). Не форсируй и не коммить за него: назови задачу в докладе
|
||||
недоведённой и оставь ветку с worktree. Молчаливый `rebase` на грязном дереве
|
||||
всё равно откажет, но с сообщением про unstaged changes — а причина другая;
|
||||
- **ненулевой код `rebase` относится к этой ветке и только к ней.** Прерванный
|
||||
rebase в чужом worktree не трогает ни главное дерево, ни остальные ветки:
|
||||
проверь `git -C <path> status` и `git status` — обе чистые. Уводить весь батч
|
||||
@@ -286,17 +295,25 @@ rebase в файле X», а не «нераспознанное пересеч
|
||||
находки и удорожат триаж. Здесь проверяется **только то, что появилось от
|
||||
слияния**:
|
||||
|
||||
- запусти **по одному `review-specs` на каждую затронутую capability**. Набор
|
||||
назван поимённо и замеров эти проходы не делают, поэтому их допустимо гнать
|
||||
одним сообщением — это то самое отступление от последовательного режима,
|
||||
которое правило разрешает. Задание сузь до стыков: не сверять capability
|
||||
целиком заново, а искать **рассинхрон код↔спека, возникший от слияния** —
|
||||
- запусти **по одному `review-specs` на каждую затронутую capability**,
|
||||
последовательно. Параллельно — только если человек попросил явно и назвал
|
||||
набор: правило и оба его условия живут в
|
||||
`av-dev-pipeline:review-pipeline`, раздел «Режим запуска», и здесь не
|
||||
ослабляются. «Замеров не делают» основанием не является;
|
||||
- **режим у этих проходов особый, и его надо назвать в задании.** Живого change
|
||||
здесь нет — все заархивированы, дельта-спек не существует. Источник требований
|
||||
— **актуальные** `openspec/specs/<capability>/spec.md`, а предмет — стык:
|
||||
требование, которое одна задача выполнила, а соседняя незаметно отменила; два
|
||||
change, по-разному описавшие одно поведение;
|
||||
архивных change, по-разному описавшие одно поведение. Скажи проходу это прямо,
|
||||
иначе он пойдёт искать дельты и не найдёт ничего;
|
||||
- если задачи пересекались по файлам, добавь один `review-architecture` на
|
||||
интегрированный дифф с вопросом «не появился ли второй способ делать то, что
|
||||
уже делается» — именно он возникает, когда две задачи независимо решали
|
||||
похожее.
|
||||
похожее;
|
||||
- **заверши триажем.** Он единственный, кто агрегирует, и без него у находок нет
|
||||
ни оракула, ни пометки `инлайн`/`развилка` — а следующий абзац на неё
|
||||
опирается. Прогон из двух проходов без триажа — это сырые находки, выданные за
|
||||
разобранные.
|
||||
|
||||
Замечания отрабатывай как одиночный пайплайн: `инлайн` чини сам, `развилка` —
|
||||
вопросом в запись; после правок — снова гейт.
|
||||
@@ -307,7 +324,7 @@ rebase в файле X», а не «нераспознанное пересеч
|
||||
`git worktree prune`. Worktree и ветки **провалившихся** не трогай — они нужны
|
||||
для ручного дожатия.
|
||||
- **Записей учёта батч не трогает** — их правит пайплайн внутри сабагента на
|
||||
шаге 9а. Батч сообщает исход по каждой задаче; если какой-то сабагент дошёл до
|
||||
шаге 11. Батч сообщает исход по каждой задаче; если какой-то сабагент дошёл до
|
||||
коммита, но закрытия не сделал (плагина нет, вызов не разрешился), скажи это
|
||||
строкой — иначе задача останется открытой молча.
|
||||
- Доложи кратко:
|
||||
|
||||
@@ -25,7 +25,9 @@ description: Автономно проводит одну задачу чере
|
||||
`av-dev-pm:docs`, `av-dev-pm:tasks`. Короткое имя может разрешиться в
|
||||
устаревшую проектную копию, и это произойдёт молча.
|
||||
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
|
||||
(`.claude/skills/{task-pipeline,review-pipeline,task-batch}`,
|
||||
(`.claude/skills/` — и голые имена `task-pipeline`, `review-pipeline`,
|
||||
`task-batch`, и с префиксом проекта: `<проект>-task-pipeline`,
|
||||
`<проект>-review-pipeline`;
|
||||
`.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и
|
||||
побеждает та, что короче названа.
|
||||
|
||||
@@ -45,7 +47,7 @@ description: Автономно проводит одну задачу чере
|
||||
проекте есть свой процесс управления задачами — он и решает, что брать.
|
||||
- **Форматом задач.** Пайплайн **не правит индексы руками и не выдумывает путь
|
||||
к скрипту учёта**: он зовёт Skill `av-dev-pm:tasks`, который этим владеет
|
||||
(шаг 9а). Закрытие как таковое — его работа, и это осознанное решение с
|
||||
(шаг 11). Закрытие как таковое — его работа, и это осознанное решение с
|
||||
названной ценой: **приёмщик и исполнитель совпали**. Закрытие поэтому **не
|
||||
окончательно** — человек на сессии возвращает задачу `reopen` с причиной, а
|
||||
доклад по критериям приёмки становится единственным, по чему приёмка вообще
|
||||
@@ -259,7 +261,8 @@ description: Автономно проводит одну задачу чере
|
||||
сжатой строкой. Отчёт, из которого исчезло «что проверить было невозможно»,
|
||||
превращается в ложное ощущение проверенности.
|
||||
|
||||
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`) — это
|
||||
**Отчёт триажа сохрани вместе с change (`openspec/changes/<id>/review/`; шаг 8
|
||||
унесёт его в `openspec/changes/archive/<id>/review/` вместе с change) — это
|
||||
обязательно, а не «если удобно».** По нему потом видно, что было найдено и что из
|
||||
этого осталось в урожае. И это единственный **независимый** артефакт о составе
|
||||
прогона: под оркестратором `task-batch` именно по нему сверяют полноту ревью
|
||||
@@ -295,7 +298,7 @@ description: Автономно проводит одну задачу чере
|
||||
основной ветке — коммит идёт прямо в неё; под оркестратором `task-batch` HEAD на
|
||||
ветке задачи в изолированном worktree, и делать дополнительно ничего не нужно.
|
||||
|
||||
Сообщение — по-русски, скиллом `commit`, если он подключён (первая строка «что
|
||||
Сообщение — по-русски, скиллом `av-dev-git:commit`, если он подключён (первая строка «что
|
||||
сделано», тело списком 1–3 пункта, без трейлеров). Одна задача — один осмысленный
|
||||
коммит.
|
||||
|
||||
@@ -309,6 +312,15 @@ description: Автономно проводит одну задачу чере
|
||||
**Порядок обязателен.** Закрытие удаляет файл задачи; сделанное до коммита оно
|
||||
оставило бы задачу закрытой без единого следа работы, если шаг 10 упадёт.
|
||||
|
||||
**Закрытие тоже коммитится — вторым коммитом, тут же.** Удаление
|
||||
`items/<slug>.md` и правка `SPRINT.md` — это правки в рабочем дереве, и оставить
|
||||
их незакоммиченными нельзя по трём причинам: `task-batch` следом делает `rebase`
|
||||
и `worktree remove`, а те откажут на грязном дереве; закрытие, не доехавшее до
|
||||
основной ветки, оставит задачу открытой молча; и опора «`SPRINT.md` под git
|
||||
показывает, что и когда закрыто» без коммита — пустые слова. Сообщение короткое,
|
||||
про учёт, а не про работу: `закрыта задача <slug>`. Это второй коммит осознанно:
|
||||
правило «одна задача — один осмысленный коммит» про работу, а учёт — не работа.
|
||||
|
||||
Плагина в проекте нет — вызов не разрешится. Тогда **ничего не выдумывай**:
|
||||
скажи в докладе, что учёт задач остаётся за владельцем, и назови исход.
|
||||
|
||||
|
||||
@@ -88,7 +88,9 @@ capability: незаполненный канон это переходное с
|
||||
### 1. Осмотрись
|
||||
|
||||
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
|
||||
уезжает. Плюс прочитай: `CLAUDE.md`, корневые `*.md`, `openspec/specs/` (список
|
||||
уезжает. **Но смотрит он только верхний уровень `docs/`:** упразднённое в корне
|
||||
репозитория (`BRIEF.md`) и во вложенных каталогах он не назовёт никогда, поэтому
|
||||
корневые `*.md` читаются глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
|
||||
capability), `openspec/config.yaml`.
|
||||
|
||||
### 2. Составь карту
|
||||
@@ -135,9 +137,15 @@ capability), `openspec/config.yaml`.
|
||||
пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и
|
||||
остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе.
|
||||
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он;
|
||||
8. `docs.py check` — до **отсутствия дрейфа**. Замечания (незаполненные
|
||||
плейсхолдеры, слабое упоминание capability) остаются: незаполненный канон это
|
||||
объявленное переходное состояние из шага 5, а не отказ.
|
||||
8. `docs.py check` — до **отсутствия дрейфа раскладки**. Замечания
|
||||
(незаполненные плейсхолдеры, слабое упоминание capability) остаются:
|
||||
незаполненный канон это объявленное переходное состояние из шага 5, а не
|
||||
отказ. **Пункт «задачи без цели» из вложенной проверки `tasks.py` тоже
|
||||
остаётся** и зелёным на этом шаге не станет: цели не сочиняются адаптацией
|
||||
(запрет в [tasks/references/adopt.md](../tasks/references/adopt.md)), их
|
||||
проставляет человек порциями переоценки на первой сессии. Пересчитай эти
|
||||
пункты в докладе переходного состояния — не выдавай их за поломку и не
|
||||
молчи о них.
|
||||
|
||||
### 5. Объяви переходное состояние
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ kebab-case.
|
||||
"canon": 1,
|
||||
"migrations": "internal/store/migrations",
|
||||
"tasks": {
|
||||
"sections": ["ядро", "инфра"]
|
||||
"backlog": "INDEX.md"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -297,6 +297,13 @@ kebab-case.
|
||||
каталога миграций, если БД есть; по нему `docs.py` делает сверку с
|
||||
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
|
||||
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
||||
Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`,
|
||||
`plan`, `sprint`, `rejected`, `sprint_section`, `questions_heading`,
|
||||
`criteria_heading`, `oracle_word`), и ключ пишется, лишь когда имя отличается от
|
||||
умолчания. **Секций беклога здесь нет:** их дом — заголовки `##` самого индекса,
|
||||
и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
|
||||
отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с
|
||||
задачами целиком.
|
||||
|
||||
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
|
||||
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
|
||||
|
||||
@@ -366,5 +366,7 @@ severity стоит здесь, а не выводится каждым прох
|
||||
}
|
||||
```
|
||||
|
||||
Плюс `"migrations": "<путь>"`, если есть БД, и `"tasks": {"sections": [...]}`,
|
||||
если секции беклога отличаются от умолчания.
|
||||
Плюс `"migrations": "<путь>"`, если есть БД. Ключ `"tasks"` заводится **только**
|
||||
когда имя файла или заголовка отличается от умолчания (`{"backlog":
|
||||
"INDEX.md"}`); секций беклога в нём нет — их дом заголовки `##` индекса. Состав
|
||||
ключей — [canon.md](canon.md).
|
||||
|
||||
@@ -142,14 +142,17 @@ python3 $tk sprint close --dir D # конец спринта; --d
|
||||
python3 $tk reopen <слаг> --dir D --reason … # приёмка не сошлась после закрытия
|
||||
```
|
||||
|
||||
`D` — каталог задач проекта; цепочка его разрешения и вызов из чужого контекста
|
||||
описаны в скилле `tasks` («Переносимость»). **Коды выхода** — там же: 1 это
|
||||
дрейф в беклоге, 3 это «каталога нет», и ветвиться на них надо по-разному.
|
||||
`D` — каталог задач проекта, по канону всегда `docs/tasks`; `--dir` передаётся
|
||||
явно каждой командой. Вызов из чужого контекста описан в скилле `tasks`
|
||||
(«Переносимость»). **Коды выхода** — там же: 1 это дрейф в беклоге, 3 это
|
||||
«каталога нет», и ветвиться на них надо по-разному.
|
||||
|
||||
**Слаг спринта заводит `sprint start`** (по умолчанию — дата) и пишет его в
|
||||
`SPRINT.md`; всё заведённое при открытом спринте помечается `sprint:<слаг>`
|
||||
`SPRINT.md`; всё заведённое **при открытом спринте** помечается `sprint:<слаг>`
|
||||
автоматически. Поэтому «первая порция — урожай прошедшего спринта» работает без
|
||||
чьей-либо памяти.
|
||||
чьей-либо памяти — но ровно до команды `sprint close`, которая `SPRINT.md`
|
||||
очищает. Отсюда порядок: **урожай заводится до закрытия, слаг для сессии берётся
|
||||
из отчёта `sprint close`** ([references/sprint.md](references/sprint.md)).
|
||||
|
||||
Правки задач делаются мутациями (`edit`, `move`, `close`), а не редактором:
|
||||
руками правится только тело файла. Это правило скилла `tasks`, здесь оно не
|
||||
@@ -165,7 +168,8 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
||||
пайплайн физически не мог. Теперь мост есть, и защита у трёх обходов ниже —
|
||||
**только текстовая**. Опоры, которые остались настоящими:
|
||||
|
||||
- **отчёт триажа** в `openspec/changes/<id>/review/` — независимый артефакт,
|
||||
- **отчёт триажа** в `openspec/changes/archive/<id>/review/` (до архивации —
|
||||
`changes/<id>/review/`) — независимый артефакт,
|
||||
написанный ревью, а не исполнителем; по нему сверяют состав прогона и урожай;
|
||||
- **`SPRINT.md` под git** — `git log -p` показывает, что и когда было закрыто;
|
||||
- **`reopen <слаг> --reason`** — закрытие не окончательно. Приёмка человеком на
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
2. **Сформулируй развилку** с вариантами и последствием каждого, рекомендация —
|
||||
первым вариантом.
|
||||
3. **Вынеси пачкой** через `AskUserQuestion`, не больше трёх за раз.
|
||||
4. **Ответ записывается в тело задачи**, тег снимается `edit <slug> --rm-tag
|
||||
question`, **хук переписывается**: «Решено: …» на вопрос «почему это лежит в
|
||||
беклоге» уже не отвечает.
|
||||
4. **Ответ записывается в тело задачи, раздел «Вопросы» опустошается**, тег
|
||||
снимается `edit <slug> --rm-tag question`, **хук переписывается**: «Решено:
|
||||
…» на вопрос «почему это лежит в беклоге» уже не отвечает. Опустошение
|
||||
раздела — не уборка, а условие взятия: правило и причина в скилле `tasks`,
|
||||
[references/task-format.md](../../tasks/references/task-format.md).
|
||||
|
||||
**Вопросы на задачах-кандидатах разбираются вне очереди порции** — здесь же, на
|
||||
этой сессии, даже если сама задача в порцию переоценки не попала. Иначе правило
|
||||
@@ -69,9 +71,10 @@
|
||||
задачи, заведённые за спринт; при урожае в 15 это две-три порции.
|
||||
- **Отбор порций по порядку:**
|
||||
1. **урожай спринта** — `list --tag sprint:<слаг>`: свежезаведённое ещё не
|
||||
проходило ни одной проверки на нужность. Слаг спринта берётся из
|
||||
`SPRINT.md` (его завёл `sprint start`), тег на задачах проставлен
|
||||
автоматически при заведении — руками не метят и не вспоминают;
|
||||
проходило ни одной проверки на нужность. Тег на задачах проставлен
|
||||
автоматически при заведении — руками не метят и не вспоминают. **Слаг
|
||||
берётся из отчёта `sprint close`, а не из `SPRINT.md`:** сессия идёт после
|
||||
закрытия, а закрытие этот файл очищает;
|
||||
2. дальше **по залежалости** — `list --stale`;
|
||||
3. по потребности — одна секция целиком, один тег (партия ревью), одна цель
|
||||
(`--goal`), список от пользователя.
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
след.
|
||||
|
||||
- **Сделана** — по определению готовности ниже. `close <slug> --implemented`:
|
||||
файл и строка удаляются, следом остаётся коммит. **Закрывает владелец спринта
|
||||
и только после вердикта приёмки** — см. «Кто и когда закрывает».
|
||||
файл и строка удаляются, следом остаётся коммит. **Закрывает агент-оркестратор
|
||||
последним шагом пайплайна, после коммита; приёмка человеком идёт позже и
|
||||
отменяется `reopen`** — см. «Кто и когда закрывает».
|
||||
- **Вышла из спринта** — `sprint drop <slug> --reason …`: возвращается в беклог
|
||||
с вопросом в файле и **без живого незакоммиченного предложения** — иначе при
|
||||
следующем взятии оно столкнётся с новым. Наработки, которые жалко терять,
|
||||
@@ -35,6 +36,13 @@
|
||||
сессии поднимается одной командой `list --tag sprint:<слаг>`. Спринт, закрытый
|
||||
без этого шага, оставляет находки жить в отчётах — то есть нигде.
|
||||
|
||||
**Порядок здесь обязателен: урожай заводится ДО команды `sprint close`.**
|
||||
Автотег ставится по слагу из `SPRINT.md`, а `sprint close` этот файл очищает;
|
||||
заведённое после команды остаётся без тега и в первую порцию следующей сессии
|
||||
не попадёт — молча, потому что пустой `list --tag` выглядит как «урожая не
|
||||
было». Если так уже вышло, тег ставится руками: `add … --tag sprint:<слаг>`,
|
||||
слаг берётся из отчёта `sprint close`.
|
||||
|
||||
**Провал спринта.** Сработал блокер — спринт распускается (`sprint close
|
||||
--dissolve --reason …`), недоделанное возвращается в беклог, новый набор
|
||||
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
||||
@@ -99,7 +107,8 @@
|
||||
2. **Принимает человек на сессии, а не отдельный агент.** Декорреляция
|
||||
исполнителя и приёмщика в момент закрытия **снята** (решение о снятии и его
|
||||
цена — в `SKILL.md`, «Стимулы»). Опоры остались три: сохранённый отчёт триажа
|
||||
в `openspec/changes/<id>/review/` — независимый артефакт, `SPRINT.md` под git
|
||||
в `openspec/changes/archive/<id>/review/` (до архивации — `changes/<id>/review/`)
|
||||
— независимый артефакт, `SPRINT.md` под git
|
||||
и `reopen`. Переоценка на сессии и есть момент, когда критерии видит не
|
||||
исполнитель.
|
||||
3. **Расхождение — дефект критериев.** Приёмщик правит критерии и возвращает
|
||||
|
||||
@@ -95,8 +95,9 @@ docs/tasks/
|
||||
скрипт запретит. Единственная оговорка: цель без задач неотличима — «ещё не
|
||||
разобрана» или «всё закрыто». Различает **тег `decomposed`** в мета-строке
|
||||
цели: он ставится, когда цель разложена на задачи. Тег, а не строка в теле —
|
||||
потому что проверяется механически: `check` требует его у пустой цели, а
|
||||
`check --fix` сам проставляет его цели, у которой задачи есть.
|
||||
потому что проверяется механически: `check` **напоминает** о нём у пустой цели
|
||||
(замечанием, не ошибкой — неразобранная цель это законное состояние), а `check
|
||||
--fix` сам проставляет его цели, у которой задачи есть.
|
||||
- **`[goal]` и `[epic]` — разные вещи.** Цель **постоянна**: живёт, пока живёт
|
||||
направление. Эпик **временен**: это задача, которая не мерджится целиком, её
|
||||
разбирают, и он исчезает. Два срока жизни под одним словом разъезжаются,
|
||||
@@ -163,9 +164,11 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившееся
|
||||
чини `check --fix` — он детерминированно правит то, где истина однозначна
|
||||
(секция, заголовок, дубли, хук из индекса в файл, строка в чужом индексе,
|
||||
пометка `decomposed` у цели с задачами), а неоднозначное (ссылка на исчезнувший
|
||||
файл, задача сразу в двух индексах) печатает отдельной пометкой
|
||||
`НЕОДНОЗНАЧНО` — это тебе, и это идёт строкой доклада.
|
||||
пометка `decomposed` у цели с задачами), а неоднозначное (задача сразу в двух
|
||||
индексах, нечего восстанавливать) печатает отдельной пометкой `НЕОДНОЗНАЧНО` —
|
||||
это тебе, и это идёт строкой доклада. **Ссылка на исчезнувший файл в пометку не
|
||||
попадает:** `--fix` её просто не трогает, и она остаётся `ОШИБКА` обычного
|
||||
`check` — то есть видна, но в докладе её надо назвать отдельно.
|
||||
|
||||
`--fix` правит **и файлы** — ровно в двух местах, где источник ровно один и
|
||||
выбирать не из чего: хук, оставшийся только в индексе, переезжает в мета-строку,
|
||||
@@ -250,7 +253,8 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
`question` (`edit --add-tag question`), иначе он не виден ни `list
|
||||
--questions`, ни правилу «задача с открытым вопросом в набор не берётся»;
|
||||
- **тег, который некому снять** — `question` после ответа снимается `edit
|
||||
--rm-tag question` вместе с записью ответа в тело;
|
||||
--rm-tag question` вместе с записью ответа в тело **и опустошением раздела
|
||||
«Вопросы»**: судит раздел, а не тег (`references/task-format.md`);
|
||||
- **свойство репозитория в рамках** — номер миграции, хеш, версия зависимости:
|
||||
в лежалой задаче протухает молча и становится ложной рамкой. Снимается;
|
||||
снимок берётся при постановке, а не при заведении;
|
||||
@@ -265,15 +269,19 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
просто каталог markdown. Текст задач — русский (язык документации проекта);
|
||||
зашита только латиница слага. OpenSpec ему тоже не нужен.
|
||||
|
||||
- **Каталог задач — `docs/tasks`, жёстко.** Цепочки разрешения нет: раскладка
|
||||
канона одинакова во всех проектах, и искать больше нечего. Каталога нет — код
|
||||
3 и вопрос человеку; `init` заводит его **только** когда проект действительно
|
||||
новый, а перевод чужой раскладки делает `av-dev-pm:canon`.
|
||||
- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: секции беклога и имена
|
||||
индексов, если они отличаются от умолчания. Один конфиг на весь канон, а не по
|
||||
одному на каталог.
|
||||
- **Каталог задач — `docs/tasks`, жёстко**, и `--dir` передаётся явно всегда:
|
||||
раскладка канона одинакова во всех проектах, и искать больше нечего. Каталога
|
||||
нет — код 3 и вопрос человеку; `init` заводит его **только** когда проект
|
||||
действительно новый, а перевод чужой раскладки делает `av-dev-pm:canon`.
|
||||
У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и
|
||||
полагаться на него скилл не должен: молча найденный чужой каталог это дрейф.
|
||||
- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: **имена** файлов и
|
||||
заголовков, и только если они отличаются от умолчания. Один конфиг на весь
|
||||
канон, а не по одному на каталог. Неизвестный ключ — код 3 на любой команде,
|
||||
так что лишнее слово в этом объекте останавливает работу с задачами целиком.
|
||||
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
||||
и названия — дело проекта (умолчание `ядро` / `инфра`).
|
||||
и названия — дело проекта (умолчание `ядро` / `инфра`). **В конфиге их нет** —
|
||||
второй список разошёлся бы с заголовками молча.
|
||||
|
||||
### Вызов из другого плагина
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
1. **Осмотрись.** Где лежат задачи, план, заметки. Каталог задач по канону —
|
||||
всегда `docs/tasks`. Секции беклога (`--sections`) — по умолчанию
|
||||
`ядро,инфра`; если у проекта деление другое по существу, оно называется
|
||||
здесь, а не подгоняется под умолчание, и уезжает в `docs/.pm.json`.
|
||||
здесь, а не подгоняется под умолчание, и становится **заголовками `##`
|
||||
индекса** — их единственным домом. В `docs/.pm.json` секции не пишутся.
|
||||
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
||||
прохода дадут два несогласованных состояния.
|
||||
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
|
||||
|
||||
@@ -92,9 +92,15 @@
|
||||
отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже
|
||||
отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять.
|
||||
|
||||
Ответ записывается в тело, тег снимается `edit <slug> --rm-tag question`, а хук
|
||||
переписывается: «Решено: …» на вопрос «почему это лежит в беклоге» уже не
|
||||
отвечает.
|
||||
**Ответ на вопрос — три правки, и первая обязательна.** Раздел «Вопросы»
|
||||
опустошается: ответ переезжает в тело решением, а не остаётся вопросом рядом с
|
||||
ответом. Затем снимается тег (`edit <slug> --rm-tag question`) и переписывается
|
||||
хук: «Решено: …» на вопрос «почему это лежит в беклоге» уже не отвечает.
|
||||
|
||||
**Порядок именно такой, потому что судит раздел, а не тег.** `sprint take`
|
||||
смотрит в непустой раздел и откажет взять задачу даже со снятым тегом, а `check`
|
||||
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
|
||||
опустошив раздел, — значит закольцевать себя между двумя советами.
|
||||
|
||||
## Файл цели
|
||||
|
||||
@@ -119,7 +125,8 @@
|
||||
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
||||
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
||||
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
||||
`check` требует его у цели без задач, `check --fix` сам ставит его цели, у
|
||||
`check` напоминает о нём у цели без задач замечанием — неразобранная цель
|
||||
законна и зелёного прогона не ломает; `check --fix` сам ставит его цели, у
|
||||
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
|
||||
- Цель живёт в `PLAN.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
|
||||
|
||||
|
||||
@@ -328,6 +328,19 @@ def _validate_config(data: dict, path: Path) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def config_home(root: Path) -> Path | None:
|
||||
"""Откуда настройки читаются на самом деле — и куда, значит, слать чинить.
|
||||
|
||||
Порядок тот же, что в `load_config`: `docs/.pm.json` побеждает. Без этой
|
||||
функции сообщения об ошибке звали править `.tasks.json`, который при живом
|
||||
`.pm.json` вообще не читается.
|
||||
"""
|
||||
pm = (root / PM_CONFIG_REL).resolve()
|
||||
if pm.is_file():
|
||||
return pm
|
||||
return root / CONFIG_NAME if (root / CONFIG_NAME).is_file() else None
|
||||
|
||||
|
||||
def config_problems(lay: Layout) -> list[str]:
|
||||
"""Каждый путь из конфига сверяется с диском ДО любых выводов о задачах.
|
||||
|
||||
@@ -335,8 +348,7 @@ def config_problems(lay: Layout) -> list[str]:
|
||||
check обвинять невиновных: «ссылка на несуществующий файл», хотя файл на
|
||||
месте, а мимо смотрит конфиг.
|
||||
"""
|
||||
src = lay.root / CONFIG_NAME
|
||||
where = str(src) if src.is_file() else "умолчания (файла .tasks.json нет)"
|
||||
where = str(config_home(lay.root) or "умолчания (конфига нет)")
|
||||
out = []
|
||||
if not lay.items.is_dir():
|
||||
out.append(f"{where}: items = «{lay.cfg['items']}» → {lay.items} — каталога нет")
|
||||
@@ -350,7 +362,11 @@ def config_problems(lay: Layout) -> list[str]:
|
||||
def looks_like_tasks(p: Path) -> bool:
|
||||
if (p / CONFIG_NAME).is_file():
|
||||
return True
|
||||
return (p / DEFAULTS["backlog"]).is_file()
|
||||
try: # индекс мог быть переименован через docs/.pm.json
|
||||
name = load_config(p).get("backlog", DEFAULTS["backlog"])
|
||||
except Env:
|
||||
name = DEFAULTS["backlog"]
|
||||
return (p / name).is_file()
|
||||
|
||||
|
||||
def resolve_layout(explicit: str | None) -> Layout:
|
||||
@@ -626,7 +642,8 @@ def check(lay: Layout, fix: bool = False) -> int:
|
||||
for p in problems:
|
||||
print(f"КОНФИГ {p}")
|
||||
print("\nсперва конфиг: пока он мимо, всё остальное диагностируется ложно"
|
||||
f" (правь {lay.root / CONFIG_NAME} или переименуй файлы)")
|
||||
f" (правь {config_home(lay.root) or lay.root / CONFIG_NAME}"
|
||||
f" или переименуй файлы)")
|
||||
return EXIT_ENV
|
||||
|
||||
if fix:
|
||||
@@ -1343,16 +1360,24 @@ def cmd_close(lay: Layout, a: argparse.Namespace) -> int:
|
||||
|
||||
|
||||
def git_deleted_text(path: Path) -> str | None:
|
||||
"""Текст файла из коммита, в котором его удалили. Возврат закрытой задачи
|
||||
возможен ровно потому, что удаление зафиксировано историей."""
|
||||
"""Текст закрытой задачи из истории git.
|
||||
|
||||
Два источника, и второй обязателен. Коммит удаления — обычный случай:
|
||||
закрытие уже уехало в историю. Но пайплайн закрывает задачу **последним
|
||||
шагом**, и между удалением файла и коммитом учёта есть окно, в котором
|
||||
коммита удаления ещё нет, а текст лежит в `HEAD`. Без второго источника
|
||||
`reopen` отказывал бы ровно на свежезакрытой задаче — то есть в самом
|
||||
вероятном своём применении.
|
||||
"""
|
||||
try:
|
||||
sha = subprocess.run(["git", "log", "--diff-filter=D", "--format=%H", "-n", "1",
|
||||
"--", str(path)], capture_output=True, text=True).stdout.strip()
|
||||
if not sha:
|
||||
return None
|
||||
out = subprocess.run(["git", "show", f"{sha}^:{path}"],
|
||||
capture_output=True, text=True)
|
||||
return out.stdout if out.returncode == 0 else None
|
||||
for rev in ([f"{sha}^"] if sha else []) + ["HEAD"]:
|
||||
out = subprocess.run(["git", "show", f"{rev}:{path}"],
|
||||
capture_output=True, text=True)
|
||||
if out.returncode == 0:
|
||||
return out.stdout
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
@@ -1622,6 +1647,8 @@ def cmd_sprint_close(lay: Layout, a: argparse.Namespace) -> int:
|
||||
if slug:
|
||||
print(f" урожай спринта — `tasks.py list --tag {SPRINT_TAG}{slug}`:"
|
||||
f" завести найденное по ходу обязан закрывающий спринт, а не пайплайн")
|
||||
print(f" автотег снят вместе со спринтом: заводимое СЕЙЧАС метится только"
|
||||
f" вручную — `add … --tag {SPRINT_TAG}{slug}`, иначе выпадет из урожая")
|
||||
print(" дальше — сессия: разбор вопросов → разбор спринта → переоценка → новый набор")
|
||||
return EXIT_OK
|
||||
|
||||
@@ -1782,7 +1809,20 @@ def init_files(lay: Layout, sections: list[str], plan_sections: list[str],
|
||||
cfg: dict) -> dict[Path, str]:
|
||||
out: dict[Path, str] = {}
|
||||
if cfg:
|
||||
out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False, indent=2) + "\n"
|
||||
# Дом настроек один — `docs/.pm.json`, ключ `tasks`. Писать в
|
||||
# `.tasks.json` при живом `.pm.json` значит писать туда, откуда никто
|
||||
# не читает: load_config его в этом случае игнорирует.
|
||||
pm = (lay.root / PM_CONFIG_REL).resolve()
|
||||
if pm.is_file():
|
||||
data = _read_json(pm)
|
||||
section = data.get("tasks") or {}
|
||||
if not isinstance(section, dict):
|
||||
raise Env(f"{pm}: ключ «tasks» — ожидался объект с настройками")
|
||||
data["tasks"] = {**section, **cfg}
|
||||
out[pm] = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
||||
else:
|
||||
out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False,
|
||||
indent=2) + "\n"
|
||||
out[lay.index("backlog")] = (
|
||||
"# Беклог\n\n"
|
||||
f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/<slug>.md`\n"
|
||||
@@ -1848,7 +1888,8 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
|
||||
print(f"каталог задач заведён: {root}")
|
||||
print(f" секции беклога: {', '.join(sections)}; части плана: {', '.join(plan_sections)}")
|
||||
if cfg:
|
||||
print(f" имена частей записаны в {root / CONFIG_NAME}")
|
||||
pm = (root / PM_CONFIG_REL).resolve()
|
||||
print(f" имена частей записаны в {pm if pm.is_file() else root / CONFIG_NAME}")
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user