# Процесс работы над House Plan > **Статус документа: канон** (редакция 2026-08-13, ролевое уточнение > 2026-09-13). Решения владельца, на > которых он стоит: прямые коммиты в `dev` **без PR** · канон статуса — **метки**, > имена английские · лёгкий трек **включён** · автор и ревьюер — независимые > агенты/сессии · любой агент может взять любую роль · инфраструктурные задачи > входят в общий флоу сразу на `S7-code-review`. > > **Область действия:** обязателен для владельца и для любого агента. Читается > сразу после `docs/SCOPE.md` и `AGENTS.md`, до `docs/STATUS.md`. Живёт в > репозитории: до августа 2026 канон лежал только в папке владельца, и свежий клон > его не содержал вовсе. > > **Приоритет источников.** Канонический бэклог — GitHub Issues; статус живёт в > метках и больше нигде: Project v2 не используется. При расхождении > документации с GitHub побеждает GitHub. Этот файл — единственный полный канон > процесса в репозитории; `AGENTS.md` — его короткое обязательное резюме, а > внешний `CODEX-RUNBOOK.md` — только маршрутизатор к канону и историческим > инструкциям. Текущие версии, состояние конкретных issue, runtime pins и списки > jobs не копируются в производную прозу: они читаются из своих исполняемых > источников. > При расхождении этого документа с `.github/workflows/*.yml` и `scripts/*` > побеждает **фактическая автоматизация**: она исполняется, а описание — нет. > Расхождение при этом не игнорируется, а заводится issue с меткой `process`. > > При расхождении процесса и привычки побеждает процесс. --- ## 1. Основное правило **Изменение продуктового кода без issue запрещено.** Код меняется только тогда, когда issue существует и находится в статусе «Готово к разработке» или дальше. Исключения — только §11, и каждое оставляет след. Правило работает лишь при точной границе «продуктового кода», иначе спор переносится на границу: | Класс | Что входит | Нужен ли issue | |---|---|---| | **A. Продукт** | `src/**`, `custom_components/houseplan/**/*.py`, `manifest.json`, `hacs.json`, `src/i18n/*.json`, `custom_components/**/translations/*` | **Да, обязательно.** Только из «Готово к разработке» или дальше | | **B. Гейты и инструменты** | `test/**`, `tests_backend/**`, `demo/**`, `scripts/**`, весь `.github/**`, `.githooks/**`, `rollup.config.mjs`, `tsconfig*.json`, `package.json`, `package-lock.json`, `pytest.ini`, `.gitignore`, `.gitattributes` | **Да.** Может использовать issue того изменения, которое покрывает; самостоятельная работа над гейтом получает свой issue (тип `tech-debt`) | | **C. Документация** | `docs/**`, `README*`, `CHANGELOG*`, `AGENTS.md`, `CONTRIBUTING.md`, `PROCESS*.md`, `LICENSE`, `(CODE\|SPEC)-REVIEW-*.md` | Документирование A/B в том же коммите — часть DoD своего issue. Самостоятельная работа над документацией — свой issue | | **D. Сгенерированное** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` (копия стенда `demo/srv/assets/houseplan-card.js` с #255 не коммитится вовсе) | Никогда не меняется само по себе. Коммит **только** класса D допустим лишь как релизный промоушен или как принятие эталонов с доказательством ревью | Практический смысл таблицы: «я только поправил тест» и «я только пересобрал бандл» перестают быть лазейками. Классы неупорядочены, но при пересечении путей **D сильнее A**: собранный бандл лежит внутри `custom_components/houseplan/frontend/`, и без этого правила он считался бы продуктовым исходником. **Инфраструктурная задача использует ускоренный вход в общий флоу** (решение владельца 2026-09-13, issue #562). Признак механический: **ни одного файла класса A**. Любой агент может сразу реализовать её по issue и в ветке `issue/-`, без аналитики, ТЗ, ревью ТЗ и статусов `S1`…`S6`. Когда материал готов, локальные гейты зелёные и ветка запушена, исполнитель ставит `S7-code-review`. Дальше действует тот же контроллер, что для продуктового кода: зелёное ревью сливает проверенный материал в `dev` и ставит `S8-merged`, а замечания или неудавшееся слияние возвращают задачу в `S6-in-progress`; после исправлений она снова идёт в `S7-code-review`. Отсутствие ТЗ не означает отсутствие проверки. Для инфраструктуры обязательны issue, терминальные трейлеры, соразмерные изменению зелёные гейты, хендофф с доказательствами и независимое код-ревью. Модель или имя агента процессом не предписываются. Задача, задевающая класс A хотя бы одним файлом, инфраструктурной **не является** и идёт полным флоу. «В основном инфраструктурная» не бывает: иначе это дорога, по которой продуктовые правки минуют ревью. Признак задан через класс файлов, а не через самоощущение исполнителя, именно поэтому. --- ## 2. Жизненный цикл Восемь рабочих статусов и два служебных. Полный маршрут ниже относится к продуктовым задачам. Фазы тестирования в цикле сознательно **нет**: найденные позже дефекты заводятся отдельными issue и проходят цикл заново. Issue закрывается после выпуска беты. ``` S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready → → S6-in-progress → S7-code-review ⟲ → S8-merged → закрыт при выпуске беты служебные: blocked (поверх статуса) rejected (закрыт) ⟲ — возврат на правки, не более 4 циклов (§4), на лёгком и коротком треке 2 короткий трек (`trivial`, §5.1) идёт S2-analysis → S5-ready, минуя S3 и S4 инфраструктурный трек (§1): без S → S7-code-review ⟲ S6-in-progress → S8-merged ``` Переходы `S4-spec-review` и `S7-code-review` выполняются **автоматически**: метка порождает событие, событие запускает ревью (§10.4). Остальные ставит исполнитель. ### 2.1 Новое — заведение задачи - **Кто:** любой — владелец, агент, пользователь (Telegram, GitHub). - **Вход:** проблема в пользовательских терминах; как проявляется или зачем нужно. Решение **не требуется** и не приветствуется. - **Запрещено:** ставить приоритет, оценивать, писать ТЗ, начинать код. ### 2.2 Аналитика и оценка Задача разбирается — и разобранная **сама идёт дальше**. Умолчание изменено решением владельца 2026-08-14: раньше аналитика ждала подтверждения по каждому пункту, и большинство ожиданий ничего не меняло — issue в основном описаны однозначно. - **Кто:** агент-аналитик. Владелец не утверждает переход — он правит асинхронно. - **Чек-лист**, результат — комментарием в issue: 1. дубликаты проверены (ссылки на похожие issue); 2. в скоупе по `docs/SCOPE.md` и `docs/TOUCH-SUPPORT.md`; 3. **пользовательская ценность 1–10** и **ценность для разработки** — что упрощает или разблокирует; 4. **сложность и риск 1–10** — трудоёмкость плюс вероятность задеть смежное; 5. приоритет **P1/P2/P3**; 6. тип: баг / фича / техдолг; 7. затронутые поверхности (модули, диалоги, бэкенд, i18n); 8. трек — **по умолчанию `small`** (§5). Если задача идёт полным треком, называется критерий §5, который она не проходит: «обычный трек» без названного критерия обоснованием не является. - **Оценки и приоритет ставятся метками сразу, согласие не запрашивается.** Комментарий аналитики — уведомление, а не запрос: **молчание владельца — согласие**, несогласие он выражает правкой меток или комментарием, и это не останавливает работу. Право отклонить задачу (`rejected`) остаётся за владельцем на любой стадии. - **Вопросов владельцу на этом этапе нет.** Единственный класс вопросов, который вообще задаётся владельцу, — продуктовые (§7.1: что человек видит или делает, объём видимых изменений), и их место — этап ТЗ, пачкой, с вариантами по умолчанию и `blocked`. Вопрос, который можно отложить до ТЗ, не задаётся в аналитике; вопрос, не блокирующий написание ТЗ, не задаётся вовсе — вместо него в ТЗ пишется блок принятых предположений. - **Выход:** `S3-spec` — переход выполняет сам аналитик, не дожидаясь ответа. Либо, при явном конфликте со `SCOPE.md`, — предложение отклонить с причиной: это единственный случай, когда аналитика останавливается и ждёт владельца. ### 2.3 ТЗ в работе — написание ТЗ - **Кто:** автор ТЗ, назначает себя. Статус означает «занято». - **Артефакт:** **тело issue**, раздел `## ТЗ` (решение владельца 2026-09-10, #517). Файл в `docs/specs/` не создаётся ни на одном треке: каталог — архив ТЗ до этой даты, и задачи, у которых файл уже есть, доживают по старой схеме. Доказуемость («вердикт вынесен на этом тексте») держит конвейер: в блок якорей документа ревью пишется `sha256` нормализованного тела, и правка ТЗ после зелёного ревью ТЗ приходит ревьюеру кода находкой, а не тишиной. - **Выход:** полная первая редакция по §7. ### 2.4 ТЗ на ревью - **Ревьюер ≠ автор.** Ревьюер получает issue и ТЗ, без устных пояснений автора. Его задача — не согласиться, а найти, где ТЗ не выполнимо или не проверяемо. - **Артефакт:** `docs/reviews/SPEC-REVIEW--r.md`, вердикт зелёный / жёлтый / красный. Лёгкий трек — комментарий в issue. - **High-находки блокируют.** Medium **в скоупе задачи** чинится в текущем issue: без High это жёлтый вердикт, автор правит ТЗ, фикс проходит повторный цикл. Medium **вне скоупа** — отдельный issue: чужой скоуп в этой задаче не правится. «Оставили в тексте ревью» не считается закрытием ни для одной (решение владельца 2026-08-19, #202: отдельный issue дороже правки на месте). Low либо правится, либо снимается решением ревьюера с записью. - **Выход:** «Готово к разработке» либо возврат в «ТЗ в работе» — не более 4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10). ### 2.5 Готово к разработке (DoR) Не работа, а **очередь**: единственный статус, из которого можно трогать код. Все пункты обязательны: - ТЗ существует, ревью ТЗ зелёное, ссылки issue ↔ ТЗ на месте; - **AC1…ACn** — пронумерованные проверяемые критерии приёмки; у каждого указано, чем он доказывается: `unit` / `backend` / `smoke` / `golden` / «ревью кода»; - перечислены затронутые файлы и модули; - i18n: ключи en + ru перечислены; - миграция и compatibility-поля решены по `docs/CONFIG-COMPATIBILITY.md`; - влияние на производительность и бюджеты названо (или явно «нет»); - влияние на touch по `docs/TOUCH-SUPPORT.md` (View и киоск — блокирующие); - release-артефакты названы: changelog RU+EN, документация, golden/скриншоты, performance/security — либо явное «нет»; - **откат**: как выключить или вернуть назад (флаг Labs, обратная миграция); - открытых продуктовых вопросов нет; риски перечислены. Если хоть один пункт не выполнен — статус не «Готово к разработке», как бы ни хотелось начать. ### 2.6 В разработке — реализация - **Занятие (claim):** назначить себя, поставить метку, комментарий «Взял: <роль> · сессия · ветка `issue/-`». - **WIP-лимиты:** не более **1** issue в «В разработке» на исполнителя, не более **3** одновременно на цикл релиза, не более **2** в «Код-ревью». - **Трассируемость:** ветка `issue/-`; каждый коммит несёт трейлеры `Issue: #` и `User-Visible: yes|no`. - **Автотесты — часть реализации, а не отдельная фаза.** Каждый AC, помеченный `unit`/`backend`/`smoke`/`golden`, получает свою проверку здесь же. «Тестирование вне жизненного цикла» означает отсутствие фазы ручного тестирования, а не отсутствие тестов. - **Приёмка проверяет результат для человека, а не строки реализации.** Для изменённой поверхности автор выбирает обычный сценарий и самый рискованный применимый соседний случай; у каждого должен быть наблюдаемый oracle — что пользователь видит, может сделать или что система отказывается делать. При выборе случаев коротко пройти шесть классов риска: async (порядок, отмена, устаревший ответ); данные и права (пусто, нет связи, несколько источников, отказ); геометрия (границы, стыки, трансформации); визуал (промежуточный кадр, тема, zoom/DPR); объём данных и performance; host/input (HA, кэш, mouse/touch/ keyboard). Неприменимое так и отмечается; проверка имени метода или строки исходника пользовательским oracle не считается. - **Скоуп не расширяется.** Найденное по пути становится новым issue в «Новое». Если находка блокирует — текущий issue уходит в «Заблокировано» со ссылкой. Попутных правок «раз уж я здесь» не бывает. - **Документация — в том же коммите,** что и поведение (действующая политика `docs/STATUS.md`): changelog RU+EN для пользовательского, `STATUS.md` для состояния, `DEVELOPMENT.md` для новых грабель, `ARCHITECTURE.md` для дизайна. - **Выход:** локальный гейт зелёный (§8), хендофф-комментарий (§7.2). ### 2.7 Код-ревью - **Ревьюер ≠ исполнитель**, свежая сессия без контекста реализации. - **Артефакт:** `docs/reviews/CODE-REVIEW--r.md` в действующем формате: скоуп, как проверялось (таблица гейтов с результатами), находки High/Medium/Low с воспроизведением, что проверено и корректно, чего не проверял. - **Ревьюер отвечает за полноту доказательств AC, а не заменяет их исполнение.** Каждый AC либо доказан автотестом — и ревьюер убедился, что **тест умеет падать**, — либо разобран по коду с явной записью «проверено чтением, не исполнением». Чтение кода выявляет риски, но не доказывает наблюдаемый пользовательский результат; если соразмерный исполнимый oracle возможен, его отсутствие — находка. Ревьюер отдельно сверяет применимые классы риска из §2.6 и не выдаёт запуск гейта за проверку сценария, которого в гейте нет. - **Защитный AC доказывается таблицей «чем краснеет» (#435).** Для каждого AC, заявляющего защиту — валидация, гард, лимит, отказ, инвариант, — в документе ревью обязательна строка из трёх столбцов: **AC · чем доказан** (точная команда или имя теста) **· чем краснеет** — мутация, снятая защита или отрицательная проба, с результатом прогона. Пустой третий столбец — находка Medium, а не примечание. «Тест умеет падать» без названной мутации и её вывода доказательством не является. Аудит v1.71.0-beta.1 нашёл пять контрактов #51 и #423, где тест оставался зелёным на снятой защите; все пять прошли код-ревью как доказанные, а два теста были записаны в закрытие coverage-ratchet под именами, обещавшими то, чего они не проверяли (#430). Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт в продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden): там ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов достаточно прогона со снятой защитой, приведённого в документе. Считаются **защитные AC без названного свидетеля**, а не мутанты на подсистему: у #421 мутанты были, и дыра всё равно проехала. «Сколько мутантов принесла задача» остаётся признаком — у #423 их ноль, и именно у #423 нашёлся тест, спрашивавший регулярку, находит ли она подстроку, которую сам же и вырезал. Правило не распространяется на AC, не заявляющие защиту (расположение, текст, формат вывода): там свидетель — обычное сравнение ожидаемого с фактическим, и третий столбец превратился бы в ритуал. И не отменяет «проверено чтением»: тогда во втором столбце стоит «чтением», а не имя теста, и читатель ревью видит разницу. - **High блокируют.** Medium **в скоупе задачи** чинится в текущем issue: без High это жёлтый вердикт и возврат автору, фикс проходит повторный цикл. Medium **вне скоупа** — отдельный issue (#202). - **Вердикт привязан к SHA (#312).** Все числа и факты отчёта сверяются с `git rev-parse HEAD` непосредственно перед подведением итогов, а не с SHA, зафиксированным в начале разбора: во время ревью в ветку может прилететь fix-up. Серверный стопор — шаг слияния конвейера сверяет вершину ветки с SHA материала ревью (допустим только собственный doc-коммит публикации поверх) и при расхождении отменяет слияние с возвратом в `S6-in-progress`. - **Выход:** очередь на пре-релиз либо возврат в «В разработке», не более 4 циклов (§4). Второй и последующие циклы разбираются по дельте (§2.10). ### 2.8 Закрытие после выпуска беты - **Вход:** изменение вошло в опубликованную бету/RC, CI Validate зелёный на **точном SHA** тега (промоушен-правило: ни одна фича не попадает в стабильный релиз, не побывав в бете). - **Закрывает** релиз-менеджер, не исполнитель. Комментарий закрытия: тег беты, ссылка на прогон CI, ссылка на бюллетень changelog. - **Стабильный релиз статусов не двигает** — issue уже закрыты; релизный коммит promotion-only, changelog ссылается на закрытые issue. - **Что приходит потом:** дефект, найденный на стенде, дома или пользователем, — **новый issue** типа «баг» со ссылкой на исходный. Исходный не переоткрывается. ### 2.9 Заблокировано / Отклонено - **Заблокировано:** обязательна ссылка на блокирующий issue или внешнюю причину и дата пересмотра. Без причины статус не ставится. - **Отклонено:** закрытие с записанной причиной (вне скоупа, дубликат, цена не оправдана). Тихое закрытие без причины запрещено. ### 2.10 Повторный раунд ревью — объём по дельте Решение владельца 2026-08-19 (issue #214). Относится и к ревью ТЗ, и к код-ревью, начиная со второго цикла. **Предмет повторного раунда — дельта, а не задача целиком.** Раньше объём разбора не был оговорён, промпт ревьюера для всех раундов был одинаковым, и повторный цикл заново выводил продуктовую рамку и перепроверял AC, которых правка не касалась: r2 по #150 стоил полного прогона конвейера ради одной строки в тестовой фикстуре. Порядок: 1. найти вердикт предыдущего раунда и **материал, на котором он получен**. Материал объявлен блоком «Материал раунда» в конце документа предыдущего раунда: конвейер дописывает туда SHA ветки, **дерево** материала и **блоб** каждого ТЗ вместе с командами поиска (issue #416). Блок машинный — править его руками не нужно и не следует; 2. объявить дельту: `git diff <тот SHA>..HEAD` для кода, дифф файла ТЗ либо тела issue для этапа ТЗ. **Если SHA не резолвится — это не находка, а обычное дело.** Ветку задачи между раундами перебазируют, сквошат или удаляют, и SHA умирает: по корпусу ревью таких объявлений 98 из 804. Материал в этом случае берётся по якорям, которые ребейз не меняет, потому что адресуются содержимым: ``` git log --all --format='%H %T' | grep <дерево> git log --all --find-object=<блоб> -- <путь к ТЗ> ``` Находкой остаётся другое: **SHA, мёртвый уже в момент публикации отчёта** — он означает, что значение сняли до `amend` или `rebase` и не сверили перед выводом, как требует §7.2. Это отличие не теоретическое: на #403 оба источника, автор и ревьюер, независимо назвали один и тот же осиротевший SHA, и следующий раунд восстанавливал коммит по содержимому диффа руками (issue #413). Конвейер теперь такую публикацию останавливает сам; 3. по каждой находке предыдущего раунда показать, **чем именно она закрыта** — строкой кода или текста, а не заявлением автора; 4. заново проверять только те AC, чьё доказательство дельта задевает; 5. **раздел «Унаследовано из r»** обязателен: что принято без повторной проверки, со ссылкой на документ того раунда и его материал. Без перечня сокращение превращается в молчаливое доверие. Дешёвые гейты (`typecheck`, `test`, `build` со сверкой копий бандла) гоняются в каждом раунде: код изменился, а стоят они минуты. Тяжёлые — по дельте (§10.2). **Разбор остаётся полным**, если дельта не локальна: ребейз на ушедший вперёд `dev` (после ребейза это другой код, §7.2), смена контракта поведения, задета новая подсистема, либо объём дельты сопоставим с исходной задачей. Сокращается объём **разбора, а не строгость**: правка по замечанию способна сломать AC, который предыдущий раунд признал выполненным — так появилась регрессия #102. Граница не «только находки», а «находки плюс всё, до чего дотягивается дельта». --- ## 3. Правила Продолжение черновика владельца. Каждое правило проверяемо — глазами или машиной. 1. **Никаких изменений в код, если нет issue** и он не помечен «Готово к разработке» или дальше. 2. **Issue не может быть взят в разработку**, пока у него нет ТЗ с зелёным ревью, пронумерованных AC с указанием доказательства и назначенного исполнителя. 3. **Issue не может быть взят дважды.** Занятие фиксируется назначением, меткой и комментарием с именем ветки. У одного исполнителя одновременно не более одного issue в разработке. 4. **Статус меняется до действия, а не после.** Взял — поставил метку; отдал на ревью — поставил метку. Метка, поставленная задним числом, — дефект процесса. 5. **Ровно одна метка статуса** на issue в любой момент. Ноль или две — дефект, еженедельная гигиена его показывает. 6. **Автор не ревьюит своё** — ни ТЗ, ни код. Никто не переводит свою работу через ревью-гейт. 7. **Ревью возвращает не более 4 раз.** Пятый заход — решение владельца: разделить, отклонить или арбитраж (§4). 8. **High блокирует. Medium в скоупе чинится в текущем issue** (без High — жёлтый вердикт и повторный цикл); Medium вне скоупа становится отдельным issue (#202). Low либо правится, либо снимается решением ревьюера с записью в документе. 9. **Скоуп не расширяется.** Всё найденное вне ТЗ — новый issue, а не попутная правка. Блокирующая находка отправляет текущий issue в «Заблокировано». 10. **Каждый коммит класса A и B несёт трейлер `Issue: #NN`**, ветка называется `issue/NN-slug`, а `User-Visible: yes` требует правок в **обоих** changelog в том же коммите. После `cherry-pick -x` служебная строка `(cherry picked from ...)` должна оставаться выше финального блока трейлеров: перед push проверяем порядок через `git show -s --format=full HEAD`. 11. **Документация — в том же коммите, что поведение.** Отдельным «допишу потом» коммитом документация не бывает. 12. **Сгенерированное не коммитится само по себе.** Только релизный промоушен или принятие эталонов со ссылкой на прогон CI. 13. **Golden-эталоны принимаются только** `npm run golden:accept -- --reviewed` по полному Linux-артефакту. Принятие ради зелёного CI — нарушение процесса. 14. **Issue закрывается после выпуска беты** с зелёным CI на точном SHA. Не раньше, не «по факту наличия кода», не исполнителем. 15. **Закрытый issue не переоткрывается.** Новый дефект — новый issue со ссылкой. 16. **Стабильный релиз — promotion-only:** версии, сгенерированные бандлы, changelog и release-метаданные. Продуктового кода там нет. 17. **История `dev` не перезаписывается.** На неё ссылаются теги. Нарушение исправляется следующим коммитом плюс issue с меткой `process` — не force-push'ем. 18. **AC доказывает автотест или запись ревьюера.** Фразы «проверил локально, всё работает» в процессе не существует: либо тест, который умеет падать, либо честное «проверено чтением, не исполнением». 19. **Параллельных бэклогов нет.** Планы, разборы и приоритеты живут в issue; файловые отчёты — разовые и датированные. 20. **Аварийный хотфикс — только решением владельца** и только по §11.2. --- ## 4. Лимит циклов ревью: 4 Оба ревью-гейта возвращают задачу на правки не более **4 раз**. - **Что считается циклом:** отправка на ревью → вердикт с блокирующими находками → возврат. Уточняющий вопрос без вердикта циклом не считается. - **Зелёный вердикт цикла не образует** и бюджет не тратит (решение владельца 2026-08-20, issue #227): он ничего не вернул на правки. Практический случай — зелёное ревью, слияние которого не удалось: конвейер сам предписывает ребейз и возврат метки, и этот заход не должен наказываться. Раньше счётчик считал все вердикты подряд, и на #225 последовательность жёлтый → зелёный → ребейз дала `review-4` на задаче с зелёным ревью и зелёным CI. - **Заход и цикл — разные величины.** Заход — сколько раз ревью отработало; он виден в имени документа (`-r1`, `-r2`, …) и нужен, чтобы два документа не затёрли друг друга. Цикл — единица бюджета §4. Заходов законно бывает больше, чем циклов, поэтому порог проверки 7 в `scripts/process-gate.mjs` выше лимита циклов (шесть документов = четыре цикла плюс два ребейза). - Метка `review-4` ставится, когда исчерпан **бюджет циклов**; конвейер снимать её не вправе — это решение владельца. Если бюджет пересчитан и оказался ниже лимита, конвейер сообщает пересчёт, но метку не трогает. - **Исчерпание лимита — не «пятая попытка», а разбор.** Задача уходит владельцу, решение одно из трёх: 1. **разделить** — issue закрывается как «заменён», вместо него 2–3 меньших с ясным скоупом (частый настоящий диагноз: ТЗ было слишком большим); 2. **отклонить** — цена решения оказалась выше ценности; 3. **арбитраж владельца** — владелец фиксирует решение в issue, оно принимается как есть; несогласие ревьюера записывается, но не блокирует. - **Граница между «циклом» и «новым багом»:** до закрытия беты находка ревьюера — возврат на правки; после закрытия — новый issue. Иначе лимит 4 обходится заведением issue вместо возврата. - Для лёгкого трека лимит ревью ТЗ — **2** цикла: задача на три часа, которую переписывают трижды, лёгкой не была. --- ## 5. Лёгкий трек (метка `small`) — путь по умолчанию Умолчание изменено решением владельца 2026-08-27, issue #338. Прежде полный трек был бесплатен, а выбор лёгкого требовал обоснования. Фактическая цена: **2.9 ревью-документа на задачу** в среднем и до шести на одну issue (#329, #316, #290) — при том что Medium-находки всё равно чинятся в той же задаче, без отдельного цикла. **Порог не изменился.** Критерии ниже те же и по-прежнему обязательны все одновременно. Изменилась сторона доказательства: теперь обосновывается не выбор лёгкого трека, а отказ от него — в `S2-analysis` называется критерий, который задача не проходит. Полный трек остаётся тем, чем был, для геометрии, миграций конфига и публичных контрактов: там критерии нарушаются сами, и назвать нарушенный несложно. Инверсия умолчания не отменяет ничего из §5 ниже и ничего из §4: бюджет четырёх циклов, арбитраж владельца, обязательность ТЗ на полном треке и правило «ревью до мержа» остаются как были. Меняется только стоимость пути по умолчанию. **Критерии — все одновременно; нарушенный называется явно:** - сложность и риск ≤ 3; - одна поверхность (один диалог, один модуль, один эндпоинт); - нет миграции конфига и новых compatibility-полей; - нет нового UX-контракта — меняется поведение в рамках уже описанного; - нет влияния на производительность и на touch-контракт. **Что упрощается:** - ТЗ короче: проблема · контракт · AC1…ACn с доказательством · откат (в теле issue, как и на полном треке с 2026-09-10); - ревью ТЗ — комментарий второго агента, отдельный документ не нужен; - лимит ревью ТЗ — 2 цикла. **Что не упрощается:** issue, оценка, статусы, трейлеры коммитов, changelog, **код-ревью и его документ**, закрытие после беты. Код-ревью не пропускается: оно проверяет скоуп, риски и качество доказательств, но не заменяет исполнение тестов. Единственное исключение из повторного ревью — починка упавшего предрелизного гейта, §11.4. Если по ходу выясняется, что критерий нарушен (появилась миграция, задело второй модуль) — метка `small` снимается, issue возвращается в `S3-spec` и получает полное ТЗ в теле issue по §7.1. Это не провал, это ранняя диагностика. ### 5.1 Короткий трек (метка `trivial`) Решение владельца 2026-08-13, issue #128. Лёгкий трек делает ТЗ дешёвым; короткий обходится без него совсем. **Маршрут:** `S1-new` → `S2-analysis` → `S5-ready` → `S6-in-progress` → `S7-code-review` → `S8-merged`. Стадии `S3-spec` и `S4-spec-review` пропускаются. `S2-analysis` остаётся: это комментарий, а не прогон CI, и именно там владелец решает приоритет и ценность. AC пишет автор в теле issue при переводе в `S5-ready` — до перехода, иначе ревьюеру нечего будет сверять. **Критерии, все обязательны:** - тип `bug`; - правка ограничена одной поверхностью, нового UX-контракта нет; - нет миграции конфига, новых ключей i18n, влияния на перф и touch; - AC выражаются тремя проверяемыми утверждениями или меньше; - **ожидаемое поведение уже зафиксировано** — в `docs/USER-GUIDE.ru.md`, в каноническом документе подсистемы либо однозначно в самом отчёте. Решать нечего. Если есть что решать, это `S3-spec`, и никакая экономия этого не отменяет. Метка ставится в `S2-analysis` вместе с остальными оценками, одним комментарием, где владелец утверждает и приоритет. **Что не упрощается:** issue, оценка, статусы, трейлеры, changelog и **код-ревью**. Лимит циклов код-ревью — 2, как на лёгком треке. Если по ходу выясняется, что критерий нарушен, метка снимается и issue уходит в `S3-spec` за нормальным ТЗ. Как и на лёгком треке, это не провал, а ранняя диагностика. **Чем этот трек опасен.** Он убирает единственное место, где решение проверялось до написания кода. Признак «решать нечего» держит всю конструкцию, и его нельзя подтверждать ощущением — только ссылкой на уже зафиксированное поведение. --- ## 6. Роли Один агент может исполнять несколько ролей в разных issue, но **не две роли в одном артефакте**. | Роль | Делает | Не имеет права | |---|---|---| | Аналитик | разбор, оценки, поверхности | окончательно ставить приоритет | | Автор ТЗ | раздел `## ТЗ` в теле issue | ревьюить своё ТЗ | | Ревьюер ТЗ | `docs/reviews/SPEC-REVIEW-NN-rN.md` | править ТЗ вместо автора | | Разработчик | код, автотесты, документация, changelog | ревьюить свой код, принимать golden | | Ревьюер кода | `docs/reviews/CODE-REVIEW-*-rN.md`, проверка AC | править продуктовый код | | Релиз-менеджер | пре-релиз, стабильный релиз, закрытие issue | добавлять код в релизный коммит | | Владелец | приоритет, ценность, скоуп, отклонение, арбитраж, хотфикс | — | **Правило разделения:** ревьюер работает состязательно. Ему передаётся тег или диапазон коммитов и ТЗ — не рассказ автора о том, как всё хорошо. **Роли не закреплены за моделями или именами агентов** (решение владельца 2026-09-13, issue #562). Codex, Claude или любой другой доступный агент может быть аналитиком, автором ТЗ, разработчиком, автором инфраструктурной задачи или релиз-инженером по прямой команде владельца. Разделение относится к артефакту: **автор и ревьюер — разные агенты/сессии**. Модель может совпадать, но ревьюер начинает без контекста реализации и не ставит вердикт собственной работе. Ревью ТЗ и код-ревью также идут в независимых сессиях: ревьюер кода не должен приходить с контекстом обсуждения ТЗ. Владелец сохраняет исключительные решения о приоритете, ценности, продуктовом скоупе, отклонении, арбитраже, закрытии issue и команде на выпуск. --- ## 7. Артефакты и трассируемость ### 7.1 Цепочка ``` issue #NN ↔ ТЗ тело issue, раздел `## ТЗ` (хеш тела — в якорях ревью) ↔ ревью ТЗ docs/reviews/SPEC-REVIEW-NN-rN.md (или комментарий при `small`) ↔ ветка issue/NN-slug ↔ коммиты трейлеры Issue: #NN · User-Visible: yes|no ↔ ревью кода docs/reviews/CODE-REVIEW--rN.md ↔ changelog бюллетень RU+EN со ссылкой на #NN ↔ бета тег, зелёный CI на точном SHA → закрытие ``` Обязательные разделы ТЗ: **сценарий** · **что человек увидит до и после** · проблема · скоуп и **не-скоуп** · контракт поведения · UX · модель данных и миграция · i18n · критерии приёмки AC1…ACn с указанием доказательства · план автотестов · риски · откат · release-артефакты. Два первых раздела — продуктовые, и они идут первыми не случайно. **Сценарий:** какая персона (`docs/SCOPE.md`), на какой поверхности, в какой момент это встретит. **Что человек увидит:** одной фразой, без терминов реализации. ТЗ, которое не может ответить на эти два вопроса, описывает работу, а не изменение продукта. **Размытое место не додумывается, а выносится владельцу.** Догадка, записанная как факт, — худший вид дефекта: она проходит ревью, потому что выглядит решением. Но спрашивать обо всём нельзя: владелец один, и анкета из двадцати пунктов хуже угадывания. Порог такой (решение владельца 2026-08-13). **Владельцу задаются только продуктовые вопросы** — что человек видит или делает и какой объём видимых изменений входит в этот issue. Поведение в пограничном случае; какая из персон важнее в конфликте; что считать приемлемой деградацией; относится ли смежное поведение сюда или становится отдельной задачей. **Всё, чего пользователь не наблюдает, агенты решают сами** либо согласовывают между собой: где хранится состояние, в каком модуле стоит гвард, именование, раскладка файлов, стратегия тестов, механика миграции. Решение записывается явным блоком в конце ТЗ — «принято предположительно, поменять свободно», и ревьюер вправе его оспорить. Технический спор автора и ревьюера решается вердиктом, а не владельцем; до него он доходит только при исчерпании лимита циклов (§4). **Смешанный вопрос делится, а не эскалируется целиком.** «Где живёт это состояние» — техническое. «Переживает ли оно перезагрузку страницы и общее ли оно для всех экранов» — продуктовое. Вопросы задаются **одним комментарием, пачкой**, каждый в форме: что неясно · что изменится от ответа · **предлагаемый вариант по умолчанию**. Вопрос с готовым вариантом стоит владельцу пяти секунд, вопрос без него — пяти минут. Пока ждём ответа, issue остаётся в `S3-spec` и получает `blocked`: статус не подменяется, `blocked` его дополняет, иначе конвейер считает задачу в работе, а она стоит. ### 7.2 Шаблоны комментариев Короткие и однообразные, чтобы читались и человеком, и машиной. - **Аналитика:** `Оценка: ценность N/10 · сложность N/10 · P<1-3> · тип · поверхности: … · дубликаты: … · лёгкий трек: да/нет` - **Занятие:** `Взял: <роль> · сессия · ветка issue/NN-slug` - **Хендофф:** `Сделано: … · Файлы: … · Гейты: <команда → результат> · НЕ сделано: … · Риски: … · Следующий статус: … · Новые issue: #…` - **Вердикт ревью:** `Вердикт: зелёный/жёлтый/красный · заход r · блокирующих циклов K/<лимит> · High: N · Medium: N → в задаче | #… · Документ: docs/reviews/…` («→ #…» — только у Medium вне скоупа; находки в скоупе возвращаются автору. Заход — номер прогона ревью, K — израсходованный бюджет §4: зелёные вердикты его не тратят, поэтому заход и K расходятся, #227) - **Закрытие:** `Выпущено в <тег беты> · CI: <ссылка> · Changelog: <ссылка>` **Вперёд двигает только зелёный вердикт.** Жёлтый и красный возвращают автору; разница между ними содержательна для человека, но не для маршрута. Первая редакция конвейера (§10.4) пропускала жёлтый при `High: 0`, и первый же живой прогон показал, почему это неверно: жёлтый там означал, что AC описывает неверное изменение контракта — реализовать такое ТЗ значило бы сделать ошибку по инструкции. ### 7.3 Расхождения с текущим состоянием, которые надо закрыть 1. ✅ **Статус ТЗ дублировал статус issue.** Закрыто 2026-09-10 (#517): ТЗ живёт в теле issue, `docs/specs/` — архив, индекс с колонкой «Статус ТЗ» удалён вместе с самой таблицей. Статус задачи — только метка `S*`. 2. **Ревью до релиза 1.62 живут вне репозитория.** Документы `CODE-REVIEW-*.md` и `SPEC-REVIEW-*.md` за прежний период лежат в папке владельца, и переносить их задним числом смысла нет: они описывают код, которого уже нет. Новые документы ревью кладёт в `docs/reviews/` сам конвейер, в ветку задачи. --- ## 8. Гейты **Локальный гейт перед выходом из «В разработке»** — минимальный набор, покрывающий изменённые поверхности (действующее правило владельца): ``` npx tsc --noEmit npm test npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js \ # копия стенда собирается `npm run bundle:sync`, в репозитории её нет (#255) node scripts/smoke-select.mjs --base origin/dev --head HEAD # какие смоки относятся к диффу node demo/smoke_<целевые>.mjs node scripts/no-new-any.mjs --base origin/dev --head HEAD # новый код не добавляет any npm run golden:verify # если менялся визуал node scripts/check-docs.mjs # если менялся src/** node scripts/model-invariants.mjs --config <экспорт> # если правилась геометрия или ссылки python -m pytest tests_backend -q # py3.14 как в CI (npm run toolchain:check), если менялся бэкенд npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \ && python tests_backend/junction_parity.py --build-dir=test-build/junction-parity # если менялось одно из зеркал junction limits ``` **Новый код не добавляет `any`** (#342). В `src/**` уже 1034 вхождения явного `any` в 49 файлах; перетипизировать это одним заходом — месяц риска ради нуля пользовательской ценности, поэтому долг снимается при плановом извлечении подсистем (#425, прежний #34), а не разовой заменой. Гейт `scripts/no-new-any.mjs` судит **только добавленные строки**: существующий долг на нетронутой строке законен, правка строки со старым `any` — новая ответственность. Исключение объявляется на той же строке, `// any-ok: <конкретная причина>`; голый маркер и причины вида «todo» не проходят. Текст разбирается парсером TypeScript, поэтому слово «any» в комментарии, строке или идентификаторе ложных срабатываний не даёт. **Объём гейтов на код-ревью соразмерен задаче** (issue #127). Всегда: `typecheck`, `npm test`, `npm run build` со сверкой трёх копий бандла, а при любом diff'е по `src/**` — ещё и `node scripts/check-docs.mjs`. По необходимости, определяемой diff'ом и AC: браузерные смоки (сколько их — считает `ls demo/smoke_*.mjs | wc -l`, вшитое число здесь трижды отставало от дерева; прогон всех уместен только когда задача задевает всё; какие относятся к диффу, печатает `node scripts/smoke-select.mjs --base origin/dev --head HEAD`, и его вывод прикладывается к ревью вместе с решением по каждой строке), `golden:verify` при изменении видимого результата, `pytest tests_backend` при правках в Python, performance-профили при названном в AC влиянии. **Полные наборы — предрелизный гейт, а не гейт ревью.** Скриншоты снимаются **только** джобой `Docs screenshots` (`workflow_dispatch`) и принимаются локально: `npm run docs:accept -- --reviewed --from=<распакованный артефакт>` (#246). Съёмка на своей машине даёт байтово другой PNG при том же кадре, и набор из «не того» браузера переписывает все десять файлов без единого содержательного изменения. Приёмка отказывает, если кандидат снят не с этого дерева, не тем капчуром, не называет свой Chromium или неполон; коммит делает человек. Когда правка `src/**` кадров не меняет — а это большинство правок — CI-цикл не нужен (#512): `npm run docs:accept -- --identical` снимает кадры локально, декодирует оба набора в Chromium и при нуле отличающихся пикселей во всех кадрах обновляет только отпечаток исходников в `screenshots.json`; байты закоммиченных PNG, их sha, браузер и упаковщик съёмки остаются прежними. Хотя бы один отличающийся пиксель — отказ с перечнем кадров и штатный путь через артефакт. `check-docs` стоит в обязательной части не по важности, а по механике: отпечаток скриншотов документации считается по всему `src/**`, поэтому **любая** правка фронтенда делает его устаревшим. Выборка «по diff и AC» здесь не работает — diff всегда попадает, и решать нечего. Цена пропуска измерена: скриншоты не пересняли в #230 и #234, и `dev` стоял с красным job `docs`, пока это не нашли при следующей задаче (#237). Пересъёмка — `npm run build && node demo/docs/capture.mjs`, коммит вместе с задачей. **Перф-смок в Validate зависит от диффа** (#473). Два glow-профиля гоняются всегда; при правке `src/iso-*` добавляется `large-house-isometric-v1`, при правке `src/live-*`, `src/render-*`, `houseplan-render-lifecycle.ts`, `houseplan-card.ts` — `large-house-interaction-v1`, оба по три образца против абсолютных потолков `hardMaxMs` полных профилей (`budgets-*-smoke.json`). Это гейт на «в разы», а не «на проценты»: регрессия #160 (первый кадр 9 870 мс против потолка 3 500) ловится ещё в ревью, а не предрелизным гейтом под тегом. Классификацию делает `scripts/classify-changes.mjs`, набор профилей входит в ключ переиспользования `performance_smoke`. Ревьюер по-прежнему принимает зелёный Validate на SHA как подтверждение дешёвых гейтов — смок его часть. Условие честности такого сужения: ревьюер обязан перечислить, какие гейты прогнал, какие нет и почему. Непрогнанный гейт становится видимым решением, а не молчаливым пропуском. **Одно число — один источник.** Любая величина, которую пользователь видит дважды — превью против записи, подпись против площади, подсветка инструмента против сохранённого значения, — обязана считаться в одном месте. Три дефекта подряд имели ровно эту причину: #234 (резинка показывала 12 см, запись хранила 24), #233 (подпись мерила по осевым линиям, площадь рядом — по полу) и способ, которым #234 нашли (подсветка «Толщины» врала согласованно с записью). Ревьюер отвечает на вопрос прямо: какое число в этом диффе видно дважды и один ли у него источник. Механическая часть правила закреплена тестом `test/single-source-numbers.test.mjs` — строку с единицей измерения собирает только канонический форматтер; смысловая часть остаётся за ревью. **Гейт беты** (условие закрытия issue): CI Validate зелёный на точном SHA тега. Часть гейтов запускается только здесь, то есть **после** пройденного код-ревью. Упавший предрелизный гейт автор чинит и повторно прогоняет; зелёный прогон достаточен для продолжения релиза, повторное код-ревью не требуется — §11.4. **Гейт стабильного релиза:** полный локальный прогон плюс Validate и Full Performance зелёные на точном SHA, плюс зелёный E2E на реальном Home Assistant: `release.yml` сам запускает `e2e.yml` в `houseplan-e2e` на SHA кандидата и ждёт его зелёного (#514, #540); установочные ассеты публикуются только после всех гейтов и только этим workflow — релиз, опубликованный руками, возвращается в черновик до их прохождения (#540); статусов issue не касается. --- ## 9. Метки — канонический статус Статус читается из меток: их видно в списке issue, их читает любой токен с доступом к Issues, и по ним же работает конвейер — смена метки порождает событие (§10.4). **Project v2 не используется** (решение владельца 2026-08-14): второе представление статуса рядом с метками требовало отдельного скоупа токена, синхронизации и внимания, а давало вид доски. Два источника одного факта расходятся — это уже случалось с колонкой «Статус ТЗ» в `docs/specs/README.md`. **Имена меток английские** (решение владельца 2026-08-12). Русские имена в этом документе были только на бумаге; репозиторий с самого начала жил на английских. | Метка | Статус | |---|---| | `S1-new` | Новое, не разобрано | | `S2-analysis` | Аналитика и оценка | | `S3-spec` | ТЗ в работе | | `S4-spec-review` | ТЗ на ревью | | `S5-ready` | Готово к разработке — единственный статус, из которого можно начать трогать код | | `S6-in-progress` | В разработке, занято исполнителем | | `S7-code-review` | Код-ревью | | `S8-merged` | Ревью пройдено, код в `dev`, ждёт беты. Issue закрывается пачкой при выпуске | | `blocked` | Ждём внешнего или владельца, **поверх** статусной метки | | `rejected` | Отклонено, issue закрыт | Модификаторы: `small` (лёгкий трек, сложность ≤3), `trivial` (короткий трек, §5.1), `hotfix`, `process`, `review-4`; приоритет `P1`/`P2`/`P3`; тип `bug`/`feature`/`tech-debt`. Тематические метки (`polish`, `infra`, `tests`, `docs`, `security`, `vacuum`) ортогональны процессу. Инварианты: продуктовая задача в процессе несёт **ровно одну `S*`-метку**. Инфраструктурная задача может не иметь `S*` во время первоначальной реализации; с первого `S7-code-review` на неё действует тот же инвариант ровно одной метки. Закрытый issue статусных меток не несёт; `blocked` не заменяет статус, а дополняет его. **Чужой issue берётся в работу так же, как свой — после явного решения владельца** (решение владельца 2026-08-13, уточнено в тот же день). Репозиторий публичный, отчёты заводят и посторонние; проверка стоит **на входе**, а не на каждом шаге. Для продуктовой задачи входом служит присвоение первой статусной метки. Для инфраструктурной — явное назначение владельцем; до готовности к первому код-ревью она может оставаться без `S*`. Как только продуктовая задача вошла в полный маршрут либо инфраструктурная получила `S7-code-review`, **кто её завёл, дальше не имеет значения** — статусы, ревью и лимиты работают одинаково. Присвоение метки и есть то самое явное решение, причём проверенное платформой: метки может ставить только тот, у кого есть право записи в репозиторий. Прежняя редакция требовала переоформлять чужой отчёт своим issue со ссылкой на исходный; это оказалось работой впустую — на #123 к моменту отказа ТЗ уже было написано. `S8-merged` появился позже остальных и закрывает разрыв, который раньше закрывался памятью человека: код принят, но бета ещё не вышла, и issue закрывать рано. Без него принятая задача либо висела в `S7-code-review`, либо закрывалась досрочно. --- ## 10. Механизация при прямых коммитах в `dev` Решение владельца — работать без PR. Значит, GitHub не может ничего заблокировать на своей стороне: **основной гейт переезжает на клиента, CI остаётся страховкой.** ### 10.1 Хуки, которые невозможно забыть поставить `.githooks/` в репозитории, `core.hooksPath` выставляется автоматически при установке зависимостей: ```json "scripts": { "prepare": "node scripts/install-hooks.mjs" } ``` `npm ci` вызывает `prepare` сам — значит, хуки появляются в каждом окружении, включая свежий контейнер облачного агента, без отдельного шага в инструкции. - **`commit-msg`** — есть, работает. Отклоняет коммит без терминального `Issue: #NN`, требует ровно один `User-Visible: yes|no`, а для коммитов, трогающих `demo/golden/baselines/**`, — `Release:` плюс `Baseline-Reviewed:`. Реализация — `scripts/validate-commit-provenance.mjs`, тот же скрипт вызывается job `provenance` в `validate.yml`. - **`pre-push`** — есть, работает. Прогоняет `scripts/process-gate.mjs` по каждому пушимому ref и останавливает push при нарушении. Это и есть блокирующий гейт вместо PR. Удаление ветки и теги пропускаются: в первом случае проверять нечего, во втором коммит уже проверен, когда его пушили. Для новой ветки диапазон считается от `merge-base` с `origin/dev`, а не от начала истории — иначе в него попали бы все нарушения, совершённые до появления гейта. При возврате `main` в `dev` диапазон merge-коммита содержит второй родитель — уже опубликованные в `main` коммиты с закрытыми issue. Для destination `dev` общий скрипт pre-push/CI исключает только SHA, доказанно достижимые из `origin/main`; сам merge и новые post-merge коммиты остаются под всеми проверками. На `main`, beta/issue-ветки и обычный push в `dev` это исключение не распространяется (issue #155). Проверка статуса issue требует `gh`, поэтому при его отсутствии хук печатает предупреждение и выполняет только офлайн-часть. Это сознательная уступка: хук, который не работает в самолёте, отключают целиком, а строгий проход всё равно делает CI. **Хук обязан быть исполняемым, и это тише всего ломается.** Git **молча** не запускает файл без бита `+x`: гейт сообщает об успехе тем, что его нет. Проверено на настоящем push — при `644` от гейта ноль строк и push проходит, при `755` он останавливается. Через GitHub API режим не выставляется: файл, отправленный так, приезжает `100644`. Поэтому `scripts/install-hooks.mjs` восстанавливает бит при каждой установке зависимостей, а `assertHookMode` дополнительно проверяет бит `.githooks/commit-msg` в индексе. Правится вручную: `git update-index --chmod=+x .githooks/<хук>`. ### 10.2 Что проверяет `process-gate.mjs` Реализовано, `scripts/process-gate.mjs`, issue #105. Офлайн, без GitHub API: 1. трейлер `Issue: #NN` у каждого коммита класса A/B, допускается несколько; 2. имя ветки `issue/NN-slug` соответствует трейлерам; 3. у класса A есть ТЗ: раздел `## ТЗ` или хотя бы один `AC1` в теле issue — либо архивный `docs/specs/NN-*.md` у задачи до 2026-09-10. Офлайн тела нет, и проверка молчит; с `--issues` — предупреждение (настоящий рубеж — ревью ТЗ). Добавление нового файла в `docs/specs/**` тоже предупреждение: каталог заморожен (#517); 4. `User-Visible: yes` → правки в обоих changelog в том же коммите; 5. коммит только класса D невалиден без `Release: vX.Y.Z` либо `Baseline-Reviewed: <ссылка на прогон CI>`; 6. релизный коммит не содержит изменений в `src/` и `custom_components/**/*.py`; 7. документов ревью на один issue не больше четырёх (`-r1`…`-r4`). С токеном GitHub: 8. `--issues` тянет каждый упомянутый issue и требует метку из {`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`}; закрытый, недоступный или помеченный `blocked` — отказ (**fail closed**). Три оговорки к проверке 8 выяснились при реализации. **`S8-merged` входит в множество**, хотя по смыслу задача уже принята. Причина механическая: конвейер (§10.4) сливает ветку в `dev` **раньше**, чем ставит метку, Validate стартует от этого push и успевает прочитать issue уже в `S8-merged`. Строгое множество красило бы каждую принятую задачу. Локальная строгость возвращается флагом `--no-merged`. **Статус спрашивается только у коммитов класса A/B.** Правило №1 говорит о продуктовом коде и инструментах, а не о документации. Иначе краснел бы каждый документ ревью: он ложится в ветку задачи, пока та в `S4-spec-review` или `S7-code-review`, то есть заведомо вне рабочего множества. **При продвижении в `main` не перепроверяются коммиты, уже достижимые из prerelease-тега.** После выпуска беты их issue по §2.8 должны быть закрыты, а stable fast-forward снова включает эти коммиты в диапазон `old-main..candidate`. Pre-push передаёт целевую remote ref через `--target-ref`, а Validate — через `TARGET_REF`; оба исключают только уже опубликованную prerelease-историю. Любой post-beta коммит остаётся в проверке и по закрытому issue отклоняется fail-closed. Не реализовано и остаётся долгом: 9. `npm run release:prerelease -- --issues=…` не проверяет, есть ли у issue зелёный вердикт код-ревью. Закрытие вошедших issue автоматизировано (#120, #547). При старте беты текущая очередь S8 служит только списком кандидатов: `RELEASE-MEMBERSHIP.json` оставляет из неё лишь номера с доказанным `Issue: #NN` в Git-диапазоне зафиксированного SHA. Manifest публикуется и входит в `SHA256SUMS`; свежая очередь S8 после публикации не перечитывается. Общий для workflow и локальной команды bookkeeping идемпотентно добавляет один маркированный release-комментарий, снимает все статусные метки и закрывает issue. Поэтому повтор после сбоя продолжает manifest, даже если метка уже снята или release уже public; автор issue на membership не влияет. ### 10.3 Страховка и разбор - **`process-gate.mjs` — job `process-gate` в `validate.yml`**, без `needs`: краснеет сам и не роняет остальные. При прямом push проверка догоняющая: код уже в `dev`, CI краснеет после. Это принятая цена отказа от PR: `pre-push` ловит нарушение до отправки, а этот job — то, что прошло мимо хука, включая `--no-verify` и окружение без установленных зависимостей. - **Нарушение не откатывается force-push'ем** (правило 17): исправляющий коммит плюс issue с меткой `process`. Починить надо проверку, а не только симптом. - **Еженедельная гигиена** (workflow): issue в `S1-new` дольше 14 дней и в `S6-in-progress` дольше 7; issue класса A в `S5-ready` без ТЗ; issue с нулём или двумя `S*`-метками; коммиты без трейлера за неделю — **цель 0**; rework rate и число issue, дошедших до `review-4`; **баги, заведённые после закрытия беты** — прямая цена отказа от фазы тестирования. ### 10.4 Событийный конвейер: метка как триггер `.github/workflows/process.yml`, issue #114. Смена статусной метки — не запись в журнал, а **сообщение**: она порождает событие, событие запускает следующий шаг. ``` S4-spec-review → ревью ТЗ → S5-ready либо возврат в S3-spec S7-code-review → код-ревью → слияние в dev → S8-merged либо возврат в S6-in-progress ``` Текущая техническая реализация независимого ревьюера — `anthropics/claude-code-action`; это деталь автоматизации, а не закрепление роли или вида задач за Claude. Ревьюер читает `docs/SCOPE.md`, `AGENTS.md`, этот документ и тело issue, публикует разбор комментарием, заводит issue на Medium-находки вне скоупа задачи (#202), кладёт документ в `docs/reviews/` ветки задачи и возвращает вердикт структурированным JSON. **Метку переставляет отдельная детерминированная стадия по вердикту, а не модель.** Четыре вещи, без которых конвейер молча не работает: 1. метки переставляет **PAT**, а не `GITHUB_TOKEN`: GitHub намеренно не порождает события от `GITHUB_TOKEN`, чтобы не было циклов, и цепочка обрывалась бы после первого шага без ошибок в логах; 2. `process.yml` обязан лежать в **ветке по умолчанию**: для события `issues` GitHub берёт workflow только оттуда, независимо от содержимого `dev`; 3. слияние в `dev` происходит **до** простановки `S8-merged`, иначе метка врёт в промежутке — она утверждает, что код в `dev`; 4. многострочный текст внутри `run:` — только через heredoc: строка с нулевым отступом обрывает блок YAML, и скрипт обрезается без ошибки парсера. **Ревью не начинается на красном коде** (#510). После фиксации материала конвейер запускает Validate с мутантами по диффу на этом SHA (`scripts/validate-gate.mjs`: `workflow_dispatch validate.yml -f mutants=true`) и ждёт его до 45 минут. Красный или пропавший прогон возвращает задачу в `S6-in-progress` с комментарием и ссылкой — код никто не читал, цикл ревью не израсходован. Мутанты по диффу вообще бегут только по запросу: на кандидате ревью, кандидате слияния (#492), кандидате беты, в ночном прогоне и на PR; обычный push обходится дешёвыми гейтами (~3 минуты). За 08–09.09 мутанты на каждом промежуточном пуше стоили 48 из 56 часов job-минут Validate и в основном отменялись следующим пушем. Ожидание gates, работа модели и публикация/интеграция — три независимых jobs (#551) с отдельными бюджетами 55, 45 и 55 минут. Поэтому долгий Validate не съедает время модели, а ожидание кандидата после зелёного вердикта не обрывает готовый review. Между jobs передаётся запечатанный artifact: run/attempt, issue, этап, раунд, branch, SHA/tree материала, якоря ТЗ и результат Validate. Получатель сверяет полный набор файлов, SHA-256 и все поля с outputs предыдущей стадии; неполный, чужой или устаревший результат fail-closed не публикуется и не разрешает merge. Timeout/cancel/failure называет конкретную стадию и оставляет метку на месте; если модель не запускалась, цикл ревью не расходуется. Длительности всех трёх стадий печатаются отдельной таблицей в summary прогона. Каждый раунд ревью платит только за то, что в нём изменилось (#518). Свидетель судится по **области своего якоря** — строкам патча плюс сорок строк с каждой стороны (`ANCHOR_RADIUS_LINES`): и в отпечатке журнала (#481), и в отборе по диффу, который читает ханки `git diff --unified=0`. Сторона гарда осталась файловой: у гарда якоря нет. Неоднозначный якорь и непрочитанные ханки дают прежний широкий ответ — незнание не доказательство. Приближение того же класса, что и сам отбор по диффу; нижняя граница — ночной полный гейт (#513). Шард считает свой план до установки окружения и при пустом плане не платит за npm ci, Python и Chromium, оставаясь исполненной job: доказательство гейта требует успешной job, а не пропущенной. **Один хендофф — один пуш.** Перед пушем — локальный `node scripts/process-gate.mjs --issues` при доступном `gh` (хук без `gh` статус issue не проверяет и молчит); после `S7-code-review` в ветку не пушить, пока не пришёл вердикт или возврат: пуш поверх идущего ревью отменяет его и стоит 10–20 минут раннера, а после фиксации материала — ещё и слияние (#312). `S7` ставится один раз на заход, не после каждого фикса CI: красный Validate конвейер вернёт сам. **Автор обязан дождаться вердикта, а не заканчивать сессию.** Ревью идёт от десяти минут до сорока пяти. Отчёт «передал на ревью» останавливает конвейер там, где он мог идти сам: вердикт придёт, а подхватить его будет некому. У агента нет часов — он существует только в момент своего хода, поэтому ожидание это опрос: раз в 90 секунд, не более 110 попыток (запас на три независимых бюджета #551) — `node scripts/wait-verdict.mjs --issue NN` делает его детерминированно и говорит только при смене состояния (#496). Смотреть на метку, а не на комментарий: метка и есть состояние. Комментарии конвейера до последнего применения `S4`/`S7` считаются историческим baseline, а уже опубликованный исход текущего раунда доставляется сразу при первом опросе (#546). При `blocked` не ждать — задача ждёт владельца. **После прогона ревью метка меняется всегда.** Инвариант появился не сразу: первая редакция при конфликте слияния оставляла метку на месте, и это оказалось тупиком — автор ждёт смену метки, метка не менялась, и он тридцать раз опрашивал впустую, чтобы отчитаться «лимит исчерпан» при зелёном вердикте. Состояние, из которого никто не может выйти и о котором никто не узнает, для конвейера хуже громкой ошибки. **Ветка приводится к `dev` до ревью, а не после** (#257). Раньше ревью читало ветку как есть, а слияние делало ребейз — проверенный SHA и слитый SHA были разными коммитами. Пока расхождение с `dev` текстовое, ребейз упирается в конфликт и это видно; смысловое расхождение git склеивает молча, и в `dev` уезжает комбинация, которую ревьюер не читал. Именно так пришёл регресс #234. Шаг перед ревью делает одно из трёх: - ветка уже содержит весь `dev` — ничего; - отстала и ребейзится чисто — ребейз, `push --force-with-lease`, ревью по приведённому состоянию. Факт ребейза передаётся в промпт, чтобы сработало правило §7.2 о полном разборе вместо дельты; - конфликт — возврат в `S6-in-progress` **до** запуска ревью. Цикл при этом не расходуется: код никто не читал, вердикта нет. Проверка стоит до ревью не только ради совпадения SHA. Конфликт всё равно вернул бы задачу, но обнаруживался он после сорока пяти минут работы ревьюера и потраченных лимитов подписки, хотя виден за пять секунд до них. `--force-with-lease` здесь обязателен с явным ожидаемым значением: между чтением ветки и пушем автор мог запушить коммит, и слепой `--force` потерял бы его молча. Расхождение lease — падение прогона, а не предупреждение. **В `dev` уезжает точный кандидат, и только проверенный** (#492, `scripts/merge-candidate.mjs`). Ревью длится десятки минут, `dev` за это время двигается; ребейз после вердикта даёт дерево, которого никто не видел, — а чистый ребейз ничего не доказывает: соседняя правка в `dev` меняет поведение без единого конфликта. Шаг слияния поэтому: - сверяет вершину ветки с материалом ревью (#312) — иначе `S6-in-progress`; - если `dev` не двигался — push с `--force-with-lease` на текущую вершину; - если двигался — ребейз (конфликт — `S6-in-progress`, как раньше), сравнение patch-id проверенного и получившегося диффа (различие — `S7-code-review`: вердикт к другому диффу не применим, §7.2), публикация кандидата в ветку задачи, запуск Validate с мутантами на ней (#510) и ожидание зелёного dispatch-прогона **на этом SHA** — push-прогон мутантов не несёт — и только затем push в `dev` с lease на ту вершину, поверх которой кандидат собран. Отклонённый lease — `dev` двинулся снова — новая попытка; после третьей — `S6-in-progress` с комментарием; - красный Validate на кандидате или прогон, не появившийся за три минуты, — `S6-in-progress` с ссылкой; `S8-merged` ставится только после push. Проверка кандидата — обычный Validate ветки: лёгкий набор плюс диффозависимые гейты. Тяжёлые гейты остаются за кандидатом релиза (#479): слияние не превращает каждое движение `dev` в двадцатиминутный прогон, а проверяет ровно то, что проверил бы пуш той же дельты. Поэтому зелёное код-ревью с неудавшимся слиянием ведёт не в `S8-merged`, а в `S6-in-progress`: работа действительно вернулась к автору, только осталась не правка кода, а ребейз. Вердикт при этом в силе, переделывать нечего. После ребейза метка `S7-code-review` возвращается и ревью идёт заново — не формальность: после ребейза на ушедший вперёд `dev` это другой код. Если метка не сменилась, значит упал сам прогон, а не работа: смотреть логи и сообщать владельцу, а не продолжать опрос. **Очередь S4/S7 сверяется отдельным bounded controller (#555).** Workflow `process-reconcile.yml` раз в полчаса делает один снимок открытых задач и завершается — активного polling одинакового состояния и вызова модели на каждый тик нет. Он сопоставляет последнюю постановку `S4`/`S7` с run по номеру задачи и этапу; если стадия успела подготовить материал, дополнительно проверяет запечатанные run/attempt, SHA/tree, список блобов ТЗ и номер раунда. Текущий `blocked`, `review-4` или снятая review-метка всегда сильнее старого события. Автоматически и не более одного раза повторно применяется только сама review-метка после доказанно потерянного события либо transient-исхода до получения запечатанного результата (`cancelled`, `timed_out`, `stale`, `startup_failure`, `skipped`). Это не применяет вердикт и не расходует цикл: обычный конвейер заново читает актуальные labels и материал. Здоровый running run не трогается. Failure guard, неизвестная связь, чужой material/stage/attempt, success без смены метки и сбой после появления `review-result`, а также потеря повторного события получают один дедуплицированный диагностический комментарий и эскалацию человеку — второй вызов модели или S8 по догадке запрещены. Перед любой записью controller перечитывает состояние; итог каждого прохода публикуется как `houseplan-process-reconcile/v1` artifact. **Конвейер — идемпотентный контроллер, а событие лишь будит его** (#499). Guard читает метки issue текущими, а не из снимка события: прогон мог простоять в очереди, пока владелец снял метку — отозванный запрос не исполняется, и комментария об этом нет. Конвейер запускают только `S4-spec-review` и `S7-code-review`; остальные метки не создают ни одной job и не входят в concurrency-группу issue — прежде любая посторонняя метка вытесняла ожидающий запуск ревью. Зелёный вердикт применяется повторно **без вызова модели**, если последний документ этапа несёт записанный конвейером вердикт `green` с High 0 и дерево материала не изменилось ни в одном файле вне `docs/reviews/**` (сравнивает `git diff` по содержимому). Ребейз, правка теста, фикстуры или ТЗ дают отличие дерева и полный разбор — правило §7.2 не ослабляется, оно просто не касается дерева, которое уже читали. Цикл считается **по этапу**: вердикт по ТЗ не расходует бюджет код-ревью. Раньше считались все вердикты подряд, и первое код-ревью #89 получило `r2/4`. --- ## 11. Исключения ### 11.1 Лёгкий трек См. §5 — это не исключение из правила №1, а более дешёвый путь по тем же статусам. ### 11.2 Аварийный хотфикс (метка `hotfix`, решение владельца) Разрешено писать код до появления issue. Обязательно: - issue создан в **той же сессии до коммита**, метка `hotfix`; - ТЗ «как сделано» + раздел «почему нельзя было ждать»; - в течение 24 часов задача ретроспективно проходит код-ревью; - аварийность названа явно в релизном хендоффе (действующее правило `AGENTS.md`). ### 11.3 Гигиена репозитория Механические изменения без изменения поведения (форматирование, мёртвые файлы) идут под квартальный umbrella-issue «Гигиена репозитория»; каждый коммит ссылается на него. Трассируемость 1:1 сохраняется. ### 11.4 Починка предрелизных гейтов без повторного код-ревью Решение владельца 2026-08-13. В цикле реализации гоняется только лёгкий набор — typecheck, unit, build (§8). Golden, браузерные смоки, performance и полный HA-харнесс запускаются перед бетой, то есть **после** того, как код-ревью пройдено и issue в `S8-merged`. Часть проблем физически не может быть найдена раньше. **Если предрелизный гейт упал, автор правит, повторно прогоняет упавшее, и зелёного прогона достаточно, чтобы релиз продолжился.** Issue остаётся в `S8-merged` и на повторное код-ревью не отправляется. Причина: полный цикл ревью в момент выпуска стоит дороже, чем риск, который он здесь снимает. Гейт уже назвал дефект точно, а исправление проверяется тем же гейтом — то есть проверка объективна и не зависит от чьего-либо суждения. **Что при этом обязательно:** - прогон упавшего гейта записан в issue: **точная команда и её результат**. «Verified» без команды доказательством не является (§8); - трейлеры на коммите как обычно, `Issue: #NN` того же issue; - при `User-Visible: yes` — правки в оба changelog в том же коммите; - эталоны golden принимаются только через `npm run golden:accept -- --reviewed` на полном артефакте Linux CI. «Чтобы гейт позеленел» основанием не является. **Границы, за которыми исключение не действует.** Оно про починку названного гейтом дефекта, а не про продолжение разработки под видом починки. Правка идёт обычным путём — новым issue либо возвратом в `S6-in-progress` — если она: - меняет контракт поведения или добавляет пользователю что-то новое; - задевает подсистему, которой в исходной задаче не было; - по объёму сопоставима с самой задачей; - меняет сам гейт вместо кода — правка теста, чтобы он перестал падать, это не починка, а сокрытие. Исключение — когда дефект **в фикстуре** и это доказано разбором, как на #89: солнце на азимуте 180° и единственное окно на северной стене, поэтому луч честно не строился. Границу определяет автор, и здесь процесс сознательно отдаёт ему то, что в остальных местах не доверяет — оценку собственной работы. Плата за скорость в единственной точке, где цикл ревью стоит дороже всего. Компенсируется тем, что запись в issue публична и релиз-менеджер видит, что именно было сделано перед выпуском. Это исключение из правила «код-ревью не пропускается никогда» (§5, §7.1) — единственное, и относится только к окну между `S8-merged` и выпуском. --- ## 12. Запрещено - код без issue или из статуса раньше «Готово к разработке»; - ТЗ, написанное после кода (кроме §11.2, и тогда с пометкой «как сделано»); - ревью своей работы; перевод своей работы через ревью-гейт; - пятый цикл ревью вместо разбора по §4; - заведение issue вместо возврата на правки, чтобы обойти лимит циклов; - принятие golden-эталонов ради зелёного CI или по частичному артефакту; - закрытие issue до выпуска беты с зелёным CI; - переоткрытие закрытого issue вместо нового бага; - Medium-находки, оставленные как TODO в документе ревью: в скоупе — чинятся в текущем issue, вне скоупа — становятся отдельным (#202); - **параллельные бэклоги** в файлах (`BACKLOG-*.md`, «планы» в docs); - ревью-документы вне репозитория; - попутные правки «раз уж я здесь»; - фича или материальное изменение поведения в стабильном релиз-коммите; - force-push в `dev`; - ручное копирование на домашний инстанс. **Нарушение процесса — тоже issue** (метка `process`): если правило удалось нарушить незаметно, виновата проверка. --- ## 13. Внедрение Состояние на 2026-08-13. 1. ✅ **Метки созданы, бэклог размечен.** У всех открытых issue владельца ровно одна `S*`-метка, инварианты чистые. 2. ✅ **Колонка «Статус ТЗ» убрана** — вместе со всем индексом: `docs/specs/` стал архивом, ТЗ переехало в тело issue (#517, 2026-09-10). Перенос старых документов ревью в `docs/reviews/` отменён: они описывают код, которого уже нет. 3. ✅ **Гейт написан** — `scripts/process-gate.mjs` плюс job в `validate.yml`, issue #105. Прошёл **вне** флоу как инфраструктурная задача (§1, issue #118), а не через ТЗ и ревью, как предполагала прежняя редакция этого пункта. 4. ✅ **Долг ревью списан решением владельца.** Беты `beta.2`…`beta.10` сделаны по прежнему процессу и не пересматриваются. Точка отсчёта — релиз 1.62.0; отсчёт начинается с первой беты следующей линии. 5. ⏳ Завести issue на находку «смок `visual_continuity` не умеет падать» — это ровно тот класс дефектов, который в процессе без ручного тестирования стоит дороже всего. 6. ✅ `BACKLOG-2026-08-11.md` — разовый отчёт, решения живут в issue. 7. ✅ `AGENTS.md` переписан целиком, шире блока §14. 8. ✅ **Канон перенесён в репозиторий** (issue #112). До этого полный процесс жил только в папке владельца, а в репозитории лежал файл на 51 строку про трейлеры коммитов — из свежего клона канон не был виден вообще. 9. ✅ **`pre-push` написан** (§10.1, issue #121). Блокирующая проверка на клиенте есть; обойти её можно только `--no-verify`, и тогда то же найдёт CI. --- ## 14. Блок для AGENTS.md ```markdown ## Процесс: код только через issue Изменение продуктового кода без issue запрещено. Код меняется только из статуса «Готово к разработке» или дальше. Полные правила, критерии статусов и гейты — `docs/PROCESS.md`, читать до начала работы. Жизненный цикл (статус = метка issue): `S1-new` → `S2-analysis` → `S3-spec` → `S4-spec-review` → `S5-ready` → `S6-in-progress` → `S7-code-review` → `S8-merged` → закрытие пачкой при выпуске беты. Оба ревью возвращают на правки не более 4 циклов; пятый заход — разбор у владельца (разделить / отклонить / арбитраж). Инфраструктурная задача (ни одного файла класса A) делается сразу любым агентом: без `S*` → `S7-code-review` ↔ `S6-in-progress` → `S8-merged`. ТЗ и ревью ТЗ нет, код-ревью обязательно. Ревью запускается **само** от меток `S4-spec-review` и `S7-code-review`. Deterministic prerequisites, модель и интеграция имеют отдельные пределы 55/45/55 минут; обычно стадии заканчиваются существенно раньше. Поставив такую метку, автор не заканчивает работу, а ждёт смены метки опросом и продолжает по тому, чем она стала. - ветка `issue/-`, коммиты с трейлерами `Issue: #NN` и `User-Visible: yes|no`; - работаем прямыми коммитами в `dev`, без PR: блокирующий гейт — локальный `pre-push` (ставится автоматически через `npm ci`), CI — страховка. Force-push в `dev` запрещён; - автор ≠ ревьюер, ни для ТЗ, ни для кода; - фазы ручного тестирования нет: автотесты пишутся в реализации, AC проверяет код-ревью, найденные позже дефекты — новые issue типа «баг»; - мелкие задачи (метка `small`, сложность ≤3): ТЗ в теле issue, ревью ТЗ комментарием, код-ревью — как обычно; - найденное вне скоупа — новый issue, а не попутная правка; - issue закрывает релиз-менеджер после выпуска беты, не исполнитель. ```