From 9ad01be81342a14ed897bc86665b4e14840ac7db Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 11:32:11 +0300 Subject: [PATCH 01/13] ci: event-driven process pipeline for spec and code review Adds .github/workflows/process.yml. A status label change is the trigger: S4-spec-review runs the spec review, S7-code-review runs the code review, and the verdict decides the next label. Only a green verdict advances; yellow and red return the task to its author. Cycle limits (4, or 2 on the light track) are counted from the verdicts already posted on the issue. Labels are moved with HP_PROCESS_TOKEN, not GITHUB_TOKEN, so the change emits an event and the chain continues. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 206 ++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 .github/workflows/process.yml diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml new file mode 100644 index 00000000..0a533721 --- /dev/null +++ b/.github/workflows/process.yml @@ -0,0 +1,206 @@ +name: Process + +# Событийный конвейер процесса (PROCESS.md). Смена статусной метки — это +# сообщение: она порождает событие, событие запускает следующий шаг. +# +# S4-spec-review -> ревью ТЗ -> S5-ready | S3-spec +# S7-code-review -> код-ревью -> S8-merged | S6-in-progress +# +# Две вещи, без которых конвейер молча не работает: +# +# 1. Метки переставляются токеном HP_PROCESS_TOKEN, а не GITHUB_TOKEN. GitHub +# намеренно не запускает workflow от событий, вызванных GITHUB_TOKEN, чтобы +# не было циклов — цепочка оборвалась бы после первого шага. +# 2. Этот файл обязан лежать в ветке по умолчанию (main). Для события `issues` +# GitHub берёт workflow только оттуда, независимо от того, что в dev. + +on: + issues: + types: [labeled] + +concurrency: + # Два события по одному issue не должны запускать два прогона. + group: process-issue-${{ github.event.issue.number }} + cancel-in-progress: false + +permissions: + contents: read + issues: write + +jobs: + guard: + runs-on: ubuntu-latest + outputs: + stage: ${{ steps.decide.outputs.stage }} + cycle: ${{ steps.decide.outputs.cycle }} + limit: ${{ steps.decide.outputs.limit }} + steps: + - id: decide + env: + GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + LABEL: ${{ github.event.label.name }} + AUTHOR: ${{ github.event.issue.user.login }} + BLOCKED: ${{ contains(github.event.issue.labels.*.name, 'blocked') }} + EXHAUSTED: ${{ contains(github.event.issue.labels.*.name, 'review-4') }} + SMALL: ${{ contains(github.event.issue.labels.*.name, 'small') }} + NUM: ${{ github.event.issue.number }} + run: | + stage="" + # Лимит циклов: 4 обычный, 2 на лёгком треке (PROCESS.md §4). + limit=4; [ "$SMALL" = "true" ] && limit=2 + + # Счётчик — по числу уже опубликованных вердиктов в issue. + done_cycles=$(gh issue view "$NUM" --repo "${{ github.repository }}" \ + --json comments -q '[.comments[] | select(.body | test("Вердикт:"))] | length') + + # В процесс идут только issue, созданные владельцем: репозиторий + # публичный, чужие отчёты бывают невалидны и статусов не несут. + if [ "$AUTHOR" != "Matysh" ]; then + echo "issue от $AUTHOR, не от владельца — пропуск" + elif [ "$BLOCKED" = "true" ]; then + echo "стоит blocked — конвейер не запускается" + elif [ "$EXHAUSTED" = "true" ]; then + echo "стоит review-4 — решение за владельцем" + elif [ "$done_cycles" -ge "$limit" ]; then + echo "циклов пройдено $done_cycles из $limit — лимит исчерпан" + gh issue edit "$NUM" --repo "${{ github.repository }}" --add-label review-4 + gh issue comment "$NUM" --repo "${{ github.repository }}" --body \ + "Лимит циклов ревью исчерпан ($done_cycles из $limit). Пятого захода нет: решение владельца — разделить задачу, отклонить или арбитраж (PROCESS.md §4)." + else + case "$LABEL" in + S4-spec-review) stage="spec" ;; + S7-code-review) stage="code" ;; + *) echo "метка $LABEL конвейер не запускает" ;; + esac + fi + echo "stage=$stage" >> "$GITHUB_OUTPUT" + echo "cycle=$((done_cycles + 1))" >> "$GITHUB_OUTPUT" + echo "limit=$limit" >> "$GITHUB_OUTPUT" + + review: + needs: guard + if: needs.guard.outputs.stage != '' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: dev + + - uses: actions/setup-node@v4 + with: { node-version: 22 } + + - name: Review + id: review + uses: anthropics/claude-code-action@v1 + with: + # Подписка, а не отдельный счёт API: токен выпускается через + # `claude setup-token` (Pro/Max). Действуют лимиты подписки. + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + Ты ревьюер проекта House Plan. Язык ответа — русский. + + Issue: #${{ github.event.issue.number }} + Репозиторий: ${{ github.repository }} + Этап: ${{ needs.guard.outputs.stage }} + spec — ревью ТЗ (PROCESS.md §2.4) + code — код-ревью (PROCESS.md §2.7) + + Прочитай в этом порядке, прежде чем судить: + 1. docs/SCOPE.md — зачем продукт существует и для кого. Он + ограничитель: «features are built, improved and accepted only + if they serve a job listed here». Первый вопрос к задаче — + какую строку Core user jobs она закрывает. + 2. AGENTS.md и PROCESS.md — процесс, классы изменений, трейлеры, + лимит циклов, формат вердикта. + 3. Тело issue #${{ github.event.issue.number }} и все комментарии. + 4. Если меняется видимое поведение — docs/USER-GUIDE.ru.md: + терминология интерфейса берётся оттуда, а не изобретается. + 5. Канонический документ затронутой подсистемы: docs/SUN.md, + LIGHT.md, CANVAS.md, WALL-THICKNESS.md, UX-MODES.md, + CONFIG-COMPATIBILITY.md, TOUCH-SUPPORT.md. + + Для этапа spec: если issue помечен small, ТЗ живёт в теле issue и + файла в docs/specs/ быть не должно. Иначе ТЗ — docs/specs/-*.md. + Проверь обязательные разделы §7.1, однозначность каждого AC и + указание способа доказательства. Отдельно проверь, что автор не + выдал догадку за решение: утверждение о поведении, которого нет ни + в одном документе и которое не помечено как предположение, — + замечание. Не бывает сложной задачи без единого открытого вопроса. + + Для этапа code: материал — diff по issue. Ручного тестирования в + цикле нет, поэтому именно ты отвечаешь на вопрос «оно вообще + работает». По каждому AC: либо он доказан автотестом и ты убедился, + что тест умеет падать, либо разобран по коду с явной записью + «проверено чтением, не исполнением». «Verified» без названной + команды и её результата доказательством не является. Проверь + трейлеры Issue и User-Visible, при User-Visible: yes — правки в оба + changelog в том же коммите. + + Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь. + + Серьёзность: High блокирует; Medium обязан стать отдельным issue; + Low либо правится, либо снимается с записью. Жёлтый вердикт + допустим при полностью выполненных AC, если изменение не решает + заявленный сценарий или ухудшает смежный. Продуктовое рассуждение + расширяет вопросы, но не отменяет AC и не даёт права менять скоуп. + + Каждую Medium-находку заведи отдельным issue со ссылкой на + #${{ github.event.issue.number }} и метками: тип, приоритет, + S1-new. «Оставили в тексте ревью» закрытием не считается и прямо + запрещено §12. + + Оставь в issue комментарий с разбором: находки с воспроизведением, + что проверено и корректно, чего не проверял. Первой строкой — + вердикт в формате §7.2: + `Вердикт: зелёный/жёлтый/красный · цикл r${{ needs.guard.outputs.cycle }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → #…` + + Затем верни JSON по схеме. + claude_args: | + --max-turns 40 + --allowedTools Read,Grep,Glob,Bash,mcp__github__add_issue_comment,mcp__github__issue_write,mcp__github__issue_read + --json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["green","yellow","red"]},"high":{"type":"integer"},"medium":{"type":"integer"},"summary":{"type":"string"}},"required":["verdict","high","medium","summary"]}' + + - name: Переставить метку + env: + # Именно PAT: с GITHUB_TOKEN следующий шаг конвейера не запустится. + GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + OUT: ${{ steps.review.outputs.structured_output }} + STAGE: ${{ needs.guard.outputs.stage }} + NUM: ${{ github.event.issue.number }} + run: | + verdict=$(echo "$OUT" | jq -r '.verdict') + high=$(echo "$OUT" | jq -r '.high') + echo "вердикт: $verdict, High: $high" + + # Вперёд двигает ТОЛЬКО зелёный. + # + # Жёлтый возвращает автору, и это не перестраховка. На первом живом + # прогоне (#111) жёлтый означал, что AC описывает неверное изменение + # контракта: реализовать такое ТЗ — сделать ошибку по инструкции. + # Разница между жёлтым и красным остаётся содержательной для человека + # и считается циклом, но ни один из них не пропускает дальше. + if [ "$verdict" = "green" ] && [ "$high" -eq 0 ]; then + case "$STAGE" in + spec) from=S4-spec-review; to=S5-ready ;; + code) from=S7-code-review; to=S8-merged ;; + esac + else + case "$STAGE" in + spec) from=S4-spec-review; to=S3-spec ;; + code) from=S7-code-review; to=S6-in-progress ;; + esac + fi + + gh issue edit "$NUM" --repo "${{ github.repository }}" \ + --add-label "$to" --remove-label "$from" + echo "$from -> $to" + + - name: Позвать владельца, если ревью упало + if: failure() + env: + GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + run: | + gh issue comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" \ + --body "Автоматическое ревью не отработало: [прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). Статусная метка не менялась, задача осталась на месте." From 9146b4c35781d2690f2ecc26e5e456bc7127b9bd Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 12:01:38 +0300 Subject: [PATCH 02/13] ci: fix OIDC permission and review the issue branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first live run failed with "Could not fetch an OIDC token": the action needs id-token: write to authenticate the GitHub App. The reviewer also checked out dev, where the material under review does not exist yet — specs and code are committed to issue/-slug. The job now switches to that branch when it is pushed, and warns loudly when it is not. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 0a533721..f6694e3b 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -26,6 +26,9 @@ concurrency: permissions: contents: read issues: write + # Обязательно: claude-code-action получает OIDC-токен для авторизации + # GitHub App. Без этого прогон падает с «Could not fetch an OIDC token». + id-token: write jobs: guard: @@ -91,6 +94,23 @@ jobs: - uses: actions/setup-node@v4 with: { node-version: 22 } + # Материал ревью живёт в ветке задачи: ТЗ в docs/specs/ и код коммитятся + # в issue/-slug. Если ветка запушена — переключаемся на неё, иначе + # ревьюер прочитает dev и не найдёт того, что должен оценивать. + - name: Перейти на ветку задачи + env: + NUM: ${{ github.event.issue.number }} + run: | + branch=$(git ls-remote --heads origin "issue/${NUM}-*" \ + | head -1 | sed 's|.*refs/heads/||') + if [ -n "$branch" ]; then + git checkout -q "origin/$branch" + echo "материал ревью: ветка $branch, $(git rev-parse --short HEAD)" + else + echo "::warning::ветка issue/${NUM}-* не найдена на origin — ревью пойдёт по dev" + echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY" + fi + - name: Review id: review uses: anthropics/claude-code-action@v1 From a29df12e0b0af3904f6a655b2cd9204e529bc146 Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 12:37:14 +0300 Subject: [PATCH 03/13] ci: raise the turn limit, bound the run by time instead The r2 spec review on #104 produced a complete green verdict and then failed on --max-turns 40 at turn 43, so the label step never ran and the transition had to be reconciled by hand. Forty was a guess; a review that reads SCOPE, AGENTS, PROCESS, the issue thread and the spec exceeds it routinely, and a code review that also runs gates needs far more. The real guard against a runaway run is the job timeout, not the turn count. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index f6694e3b..4aa0c585 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -84,7 +84,8 @@ jobs: needs: guard if: needs.guard.outputs.stage != '' runs-on: ubuntu-latest - timeout-minutes: 30 + # Время — единственный настоящий ограничитель зациклившегося прогона. + timeout-minutes: 45 steps: - uses: actions/checkout@v4 with: @@ -96,7 +97,7 @@ jobs: # Материал ревью живёт в ветке задачи: ТЗ в docs/specs/ и код коммитятся # в issue/-slug. Если ветка запушена — переключаемся на неё, иначе - # ревьюер прочитает dev и не найдёт того, что должен оценивать. + # ревьюер прочтёт dev и не найдёт того, что должен оценивать. - name: Перейти на ветку задачи env: NUM: ${{ github.event.issue.number }} @@ -149,6 +150,11 @@ jobs: в одном документе и которое не помечено как предположение, — замечание. Не бывает сложной задачи без единого открытого вопроса. + Владельцу задаются только продуктовые вопросы: что человек видит или + делает и каков объём видимых изменений в этом issue. Технический + вопрос, вынесенный владельцу, — тоже замечание: ты его снимаешь и + решаешь по существу в своём вердикте. + Для этапа code: материал — diff по issue. Ручного тестирования в цикле нет, поэтому именно ты отвечаешь на вопрос «оно вообще работает». По каждому AC: либо он доказан автотестом и ты убедился, @@ -176,9 +182,10 @@ jobs: вердикт в формате §7.2: `Вердикт: зелёный/жёлтый/красный · цикл r${{ needs.guard.outputs.cycle }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → #…` - Затем верни JSON по схеме. + Затем верни JSON по схеме. Это последнее действие и оно обязательно: + без него метка не переставится и конвейер встанет. claude_args: | - --max-turns 40 + --max-turns 150 --allowedTools Read,Grep,Glob,Bash,mcp__github__add_issue_comment,mcp__github__issue_write,mcp__github__issue_read --json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["green","yellow","red"]},"high":{"type":"integer"},"medium":{"type":"integer"},"summary":{"type":"string"}},"required":["verdict","high","medium","summary"]}' @@ -223,4 +230,6 @@ jobs: GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} run: | gh issue comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" \ - --body "Автоматическое ревью не отработало: [прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). Статусная метка не менялась, задача осталась на месте." + --body "Автоматическое ревью не отработало: [прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). Статусная метка не менялась, задача осталась на месте. + +Если вердикт выше всё же опубликован — сбой произошёл после него. Перестановку метки в этом случае выполняет чат обслуживания или владелец, но не автор задачи: автор не толкует вердикт о своей же работе." From 65a86db12289790cf8010a998ecee8f54e9dbd6a Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 12:57:04 +0300 Subject: [PATCH 04/13] fix(ci): repair the failure comment step The multi-line --body started at column zero, which ends the YAML block scalar. The parser silently truncated the run script and left an unclosed double quote, so the whole workflow became unusable and blocked the code review on #104. The body now goes through a heredoc. Validating YAML alone did not catch this; every run block is checked with bash -n from now on. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 4aa0c585..e4df3f3d 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -6,13 +6,16 @@ name: Process # S4-spec-review -> ревью ТЗ -> S5-ready | S3-spec # S7-code-review -> код-ревью -> S8-merged | S6-in-progress # -# Две вещи, без которых конвейер молча не работает: +# Три вещи, без которых конвейер молча не работает: # # 1. Метки переставляются токеном HP_PROCESS_TOKEN, а не GITHUB_TOKEN. GitHub # намеренно не запускает workflow от событий, вызванных GITHUB_TOKEN, чтобы # не было циклов — цепочка оборвалась бы после первого шага. # 2. Этот файл обязан лежать в ветке по умолчанию (main). Для события `issues` # GitHub берёт workflow только оттуда, независимо от того, что в dev. +# 3. Многострочный текст внутри `run:` — только через heredoc. Строка с нулевым +# отступом обрывает блок YAML, и скрипт обрезается без ошибки парсера. +# Проверять не только YAML, но и каждый `run` через `bash -n`. on: issues: @@ -178,8 +181,9 @@ jobs: запрещено §12. Оставь в issue комментарий с разбором: находки с воспроизведением, - что проверено и корректно, чего не проверял. Первой строкой — - вердикт в формате §7.2: + что проверено и корректно, чего не проверял. Комментарий и есть + документ ревью для прогонов в CI — отдельный файл не создаётся. + Первой строкой — вердикт в формате §7.2: `Вердикт: зелёный/жёлтый/красный · цикл r${{ needs.guard.outputs.cycle }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → #…` Затем верни JSON по схеме. Это последнее действие и оно обязательно: @@ -201,13 +205,10 @@ jobs: high=$(echo "$OUT" | jq -r '.high') echo "вердикт: $verdict, High: $high" - # Вперёд двигает ТОЛЬКО зелёный. - # - # Жёлтый возвращает автору, и это не перестраховка. На первом живом - # прогоне (#111) жёлтый означал, что AC описывает неверное изменение - # контракта: реализовать такое ТЗ — сделать ошибку по инструкции. - # Разница между жёлтым и красным остаётся содержательной для человека - # и считается циклом, но ни один из них не пропускает дальше. + # Вперёд двигает ТОЛЬКО зелёный. Жёлтый и красный возвращают + # автору: на прогоне #111 жёлтый означал, что AC описывает неверное + # изменение контракта — реализовать такое ТЗ значит сделать ошибку + # по инструкции. Оба считаются циклом. if [ "$verdict" = "green" ] && [ "$high" -eq 0 ]; then case "$STAGE" in spec) from=S4-spec-review; to=S5-ready ;; @@ -228,8 +229,14 @@ jobs: if: failure() env: GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - gh issue comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" \ - --body "Автоматическое ревью не отработало: [прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). Статусная метка не менялась, задача осталась на месте. + # Тело через heredoc, а не многострочный --body: строка с нулевым + # отступом обрывает блок YAML и оставляет незакрытую кавычку. + cat > /tmp/failure.md < Date: Thu, 13 Aug 2026 13:04:26 +0300 Subject: [PATCH 05/13] ci: the reviewer writes a review document to the task branch PROCESS.md wants a review document in docs/reviews/; the CI reviewer could only leave a comment, and flagged the gap itself. It may now write there. What lands in the commit is decided by the workflow, not by the model: every path outside docs/reviews/ is reverted before staging, and the commit carries the usual trailers so the provenance gate accepts it. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 46 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index e4df3f3d..711e97a1 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -102,6 +102,7 @@ jobs: # в issue/-slug. Если ветка запушена — переключаемся на неё, иначе # ревьюер прочтёт dev и не найдёт того, что должен оценивать. - name: Перейти на ветку задачи + id: branch env: NUM: ${{ github.event.issue.number }} run: | @@ -110,6 +111,7 @@ jobs: if [ -n "$branch" ]; then git checkout -q "origin/$branch" echo "материал ревью: ветка $branch, $(git rev-parse --short HEAD)" + echo "name=$branch" >> "$GITHUB_OUTPUT" else echo "::warning::ветка issue/${NUM}-* не найдена на origin — ревью пойдёт по dev" echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY" @@ -180,19 +182,53 @@ jobs: S1-new. «Оставили в тексте ревью» закрытием не считается и прямо запрещено §12. - Оставь в issue комментарий с разбором: находки с воспроизведением, - что проверено и корректно, чего не проверял. Комментарий и есть - документ ревью для прогонов в CI — отдельный файл не создаётся. - Первой строкой — вердикт в формате §7.2: + Напиши полный документ ревью в файл + docs/reviews/-REVIEW-${{ github.event.issue.number }}-r${{ needs.guard.outputs.cycle }}.md + (SPEC для этапа spec, CODE для code): скоуп, как проверялось, + находки с воспроизведением, что проверено и корректно, чего не + проверял. Каталог docs/reviews/ создай, если его нет. Больше не + пиши ничего: любой файл вне docs/reviews/ опубликован не будет. + + Затем оставь в issue краткий комментарий: вердикт, ключевые находки + и ссылка на документ. Первой строкой — вердикт в формате §7.2: `Вердикт: зелёный/жёлтый/красный · цикл r${{ needs.guard.outputs.cycle }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → #…` Затем верни JSON по схеме. Это последнее действие и оно обязательно: без него метка не переставится и конвейер встанет. claude_args: | --max-turns 150 - --allowedTools Read,Grep,Glob,Bash,mcp__github__add_issue_comment,mcp__github__issue_write,mcp__github__issue_read + --allowedTools Read,Write,Grep,Glob,Bash,mcp__github__add_issue_comment,mcp__github__issue_write,mcp__github__issue_read --json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["green","yellow","red"]},"high":{"type":"integer"},"medium":{"type":"integer"},"summary":{"type":"string"}},"required":["verdict","high","medium","summary"]}' + # Ревьюер пишет только в docs/reviews/. Что именно попадёт в коммит, + # решает этот шаг, а не модель: всё остальное откатывается. + - name: Опубликовать документ ревью + env: + TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + BRANCH: ${{ steps.branch.outputs.name }} + NUM: ${{ github.event.issue.number }} + run: | + if [ -z "$BRANCH" ]; then + echo "ветки задачи нет — документ некуда класть"; exit 0 + fi + git checkout -- . 2>/dev/null || true + git clean -fd -e docs/reviews >/dev/null 2>&1 || true + git add docs/reviews 2>/dev/null || true + if git diff --cached --quiet; then + echo "документ ревью не создан"; exit 0 + fi + git -c user.name="claude[bot]" \ + -c user.email="209825114+claude[bot]@users.noreply.github.com" \ + commit -q -F - < Date: Thu, 13 Aug 2026 13:20:26 +0300 Subject: [PATCH 06/13] ci: merge into dev before setting S8-merged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The label asserts the code is in dev. The workflow used to set it on a green code review while the commits were still only on the task branch, so between the verdict and the author's merge the state machine stated something untrue — which is exactly what happened on #104. The merge now runs inside the pipeline, before the label. A conflict leaves the issue in S7-code-review and comments instead. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 75 +++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 711e97a1..0f09e485 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -4,7 +4,7 @@ name: Process # сообщение: она порождает событие, событие запускает следующий шаг. # # S4-spec-review -> ревью ТЗ -> S5-ready | S3-spec -# S7-code-review -> код-ревью -> S8-merged | S6-in-progress +# S7-code-review -> код-ревью -> слияние в dev -> S8-merged | S6-in-progress # # Три вещи, без которых конвейер молча не работает: # @@ -160,14 +160,16 @@ jobs: вопрос, вынесенный владельцу, — тоже замечание: ты его снимаешь и решаешь по существу в своём вердикте. - Для этапа code: материал — diff по issue. Ручного тестирования в - цикле нет, поэтому именно ты отвечаешь на вопрос «оно вообще - работает». По каждому AC: либо он доказан автотестом и ты убедился, - что тест умеет падать, либо разобран по коду с явной записью - «проверено чтением, не исполнением». «Verified» без названной - команды и её результата доказательством не является. Проверь - трейлеры Issue и User-Visible, при User-Visible: yes — правки в оба - changelog в том же коммите. + Для этапа code: материал — диапазон `git log --oneline origin/dev..HEAD` + и `git diff origin/dev...HEAD`. Ручного тестирования в цикле нет, + поэтому именно ты отвечаешь на вопрос «оно вообще работает». + По каждому AC: либо он доказан автотестом и ты убедился, что тест + умеет падать, либо разобран по коду с явной записью «проверено + чтением, не исполнением». «Verified» без названной команды и её + результата доказательством не является. Зависимостей в рабочей + копии нет: перед гейтами выполни `npm ci`. Проверь трейлеры Issue и + User-Visible, при User-Visible: yes — правки в оба changelog в том же + коммите. Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь. @@ -212,7 +214,7 @@ jobs: echo "ветки задачи нет — документ некуда класть"; exit 0 fi git checkout -- . 2>/dev/null || true - git clean -fd -e docs/reviews >/dev/null 2>&1 || true + git clean -fd -e docs/reviews -e node_modules >/dev/null 2>&1 || true git add docs/reviews 2>/dev/null || true if git diff --cached --quiet; then echo "документ ревью не создан"; exit 0 @@ -229,13 +231,11 @@ jobs: "HEAD:$BRANCH" echo "документ опубликован в $BRANCH" - - name: Переставить метку + - name: Решение по вердикту + id: decide env: - # Именно PAT: с GITHUB_TOKEN следующий шаг конвейера не запустится. - GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} OUT: ${{ steps.review.outputs.structured_output }} STAGE: ${{ needs.guard.outputs.stage }} - NUM: ${{ github.event.issue.number }} run: | verdict=$(echo "$OUT" | jq -r '.verdict') high=$(echo "$OUT" | jq -r '.high') @@ -246,20 +246,63 @@ jobs: # изменение контракта — реализовать такое ТЗ значит сделать ошибку # по инструкции. Оба считаются циклом. if [ "$verdict" = "green" ] && [ "$high" -eq 0 ]; then + green=true case "$STAGE" in spec) from=S4-spec-review; to=S5-ready ;; code) from=S7-code-review; to=S8-merged ;; esac else + green=false case "$STAGE" in spec) from=S4-spec-review; to=S3-spec ;; code) from=S7-code-review; to=S6-in-progress ;; esac fi + echo "green=$green" >> "$GITHUB_OUTPUT" + echo "from=$from" >> "$GITHUB_OUTPUT" + echo "to=$to" >> "$GITHUB_OUTPUT" + # S8-merged утверждает, что код в dev. Значит слияние обязано произойти + # ДО метки, иначе она врờt в промежутке. Конфликт — метка не двигается. + - name: Слить ветку в dev + if: needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' + env: + TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + BRANCH: ${{ steps.branch.outputs.name }} + NUM: ${{ github.event.issue.number }} + run: | + if [ -z "$BRANCH" ]; then + echo "::error::ветки задачи нет — сливать нечего"; exit 1 + fi + git fetch -q origin dev + git checkout -q -B merge-into-dev "origin/$BRANCH" + if ! git -c user.name="claude[bot]" \ + -c user.email="209825114+claude[bot]@users.noreply.github.com" \ + rebase origin/dev; then + git rebase --abort || true + cat > /tmp/conflict.md < $to" + --add-label "$TO" --remove-label "$FROM" + echo "$FROM -> $TO" - name: Позвать владельца, если ревью упало if: failure() From fafeca4540cbcaeb234cb9bfc6e19a856b99dbac Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 16:08:20 +0300 Subject: [PATCH 07/13] fix: count review cycles per stage, not across the whole issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard counted every verdict comment on the issue, so a spec-review verdict consumed a cycle from the code-review budget. On #89 the first code review came out as r2/4. With two spec cycles the second code review would have hit review-4 after a single fix — the limit would have fired on a task nobody had reviewed twice. The stage is now resolved first and only its own verdicts are counted, recognised by the review document named in the comment. If the document is missing the verdict is not counted: undercounting grants an extra cycle, overcounting would stop the work early, and of the two mistakes the recoverable one wins. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 44 +++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 0f09e485..f07397a7 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -51,33 +51,53 @@ jobs: SMALL: ${{ contains(github.event.issue.labels.*.name, 'small') }} NUM: ${{ github.event.issue.number }} run: | - stage="" + # Этап определяется первым: от него зависит, какие вердикты считать. + stage=""; marker="" + case "$LABEL" in + S4-spec-review) stage="spec"; marker="SPEC-REVIEW" ;; + S7-code-review) stage="code"; marker="CODE-REVIEW" ;; + *) echo "метка $LABEL конвейер не запускает" ;; + esac + # Лимит циклов: 4 обычный, 2 на лёгком треке (PROCESS.md §4). limit=4; [ "$SMALL" = "true" ] && limit=2 - # Счётчик — по числу уже опубликованных вердиктов в issue. - done_cycles=$(gh issue view "$NUM" --repo "${{ github.repository }}" \ - --json comments -q '[.comments[] | select(.body | test("Вердикт:"))] | length') + # Счётчик считает вердикты ТОЛЬКО своего этапа. Раньше он брал все + # подряд, и вердикт по ТЗ съедал цикл из бюджета код-ревью: на #89 + # первое код-ревью получило r2/4. На задаче с двумя циклами ТЗ второе + # код-ревью упиралось бы в review-4 после одной правки. + # + # Этап опознаётся по имени документа в теле комментария. Если документа + # нет, вердикт не посчитается — недосчёт даёт лишний цикл, а перерасчёт + # остановил бы работу досрочно; из двух ошибок выбрана обратимая. + done_cycles=0 + if [ -n "$stage" ]; then + done_cycles=$(gh issue view "$NUM" --repo "${{ github.repository }}" \ + --json comments \ + -q "[.comments[] | select(.body | test(\"Вердикт:\")) | select(.body | test(\"$marker\"))] | length") + fi # В процесс идут только issue, созданные владельцем: репозиторий # публичный, чужие отчёты бывают невалидны и статусов не несут. - if [ "$AUTHOR" != "Matysh" ]; then + if [ -z "$stage" ]; then + : + elif [ "$AUTHOR" != "Matysh" ]; then echo "issue от $AUTHOR, не от владельца — пропуск" + stage="" elif [ "$BLOCKED" = "true" ]; then echo "стоит blocked — конвейер не запускается" + stage="" elif [ "$EXHAUSTED" = "true" ]; then echo "стоит review-4 — решение за владельцем" + stage="" elif [ "$done_cycles" -ge "$limit" ]; then - echo "циклов пройдено $done_cycles из $limit — лимит исчерпан" + echo "циклов этапа $stage пройдено $done_cycles из $limit — лимит исчерпан" gh issue edit "$NUM" --repo "${{ github.repository }}" --add-label review-4 gh issue comment "$NUM" --repo "${{ github.repository }}" --body \ - "Лимит циклов ревью исчерпан ($done_cycles из $limit). Пятого захода нет: решение владельца — разделить задачу, отклонить или арбитраж (PROCESS.md §4)." + "Лимит циклов ревью исчерпан ($done_cycles из $limit на этапе \`$stage\`). Пятого захода нет: решение владельца — разделить задачу, отклонить или арбитраж (PROCESS.md §4)." + stage="" else - case "$LABEL" in - S4-spec-review) stage="spec" ;; - S7-code-review) stage="code" ;; - *) echo "метка $LABEL конвейер не запускает" ;; - esac + echo "этап $stage, цикл $((done_cycles + 1)) из $limit" fi echo "stage=$stage" >> "$GITHUB_OUTPUT" echo "cycle=$((done_cycles + 1))" >> "$GITHUB_OUTPUT" From 9be81c141395522029952f3c5539758adc10a4df Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 16:16:39 +0300 Subject: [PATCH 08/13] fix: repair the line continuation in the failure handler The step that comments on the issue when a review run dies carried a literal backslash instead of a line continuation, so gh received four arguments and --repo ran as a command of its own. The handler for failures would itself have failed, silently, and only when something had already gone wrong. bash -n does not catch this: the syntax is valid, the meaning is not. Checking run blocks now also means looking for a doubled backslash at end of line. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index f07397a7..1f197671 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -337,5 +337,5 @@ jobs: Если вердикт выше всё же опубликован — сбой произошёл после него. Перестановку метки в этом случае выполняет чат обслуживания или владелец, но не автор задачи: автор не толкует вердикт о своей же работе. EOF - gh issue comment "${{ github.event.issue.number }}" \\ + gh issue comment "${{ github.event.issue.number }}" \ --repo "${{ github.repository }}" --body-file /tmp/failure.md From d1be6891b255017ec7656d7e1e20edbeb2bccbb6 Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 16:58:18 +0300 Subject: [PATCH 09/13] fix: a review run always moves the label, conflict or not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A green code review whose merge conflicted used to leave the label where it was. That is a dead end: the author waits for the label to change, so it polled thirty times and reported the limit as exhausted — on a task the reviewer had already passed. The verdict existed and nobody could act on it. The merge step no longer fails the job. It reports whether it merged, and a green review that did not merge sends the task back to S6-in-progress, because the work did return to the author — a rebase rather than a code fix, and the comment says so and says the verdict still stands. The invariant is now stronger and worth stating plainly: after a review run the label always changes. A pipeline whose state can stall silently is worse than one that reports the wrong state loudly. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 1f197671..00525e7d 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -283,8 +283,15 @@ jobs: echo "to=$to" >> "$GITHUB_OUTPUT" # S8-merged утверждает, что код в dev. Значит слияние обязано произойти - # ДО метки, иначе она врờt в промежутке. Конфликт — метка не двигается. + # ДО метки, иначе она врёт в промежутке. + # + # При конфликте шаг НЕ падает и метку не оставляет на месте. Первая + # редакция делала именно так, и это оказалось тупиком: автор ждёт смену + # метки, метка не менялась, и он тридцать раз опрашивал впустую, чтобы + # затем отчитаться «лимит исчерпан» — при зелёном вердикте. Инвариант + # теперь жёстче: ПОСЛЕ ПРОГОНА РЕВЬЮ МЕТКА МЕНЯЕТСЯ ВСЕГДА. - name: Слить ветку в dev + id: merge if: needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' env: TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} @@ -293,7 +300,9 @@ jobs: NUM: ${{ github.event.issue.number }} run: | if [ -z "$BRANCH" ]; then - echo "::error::ветки задачи нет — сливать нечего"; exit 1 + echo "::error::ветки задачи нет — сливать нечего" + echo "merged=false" >> "$GITHUB_OUTPUT" + exit 0 fi git fetch -q origin dev git checkout -q -B merge-into-dev "origin/$BRANCH" @@ -301,15 +310,24 @@ jobs: -c user.email="209825114+claude[bot]@users.noreply.github.com" \ rebase origin/dev; then git rebase --abort || true + echo "merged=false" >> "$GITHUB_OUTPUT" + echo "::warning::ветка $BRANCH не сливается в dev без конфликта" cat > /tmp/conflict.md <> "$GITHUB_OUTPUT" echo "слито в dev: $(git rev-parse --short HEAD)" - name: Переставить метку @@ -318,7 +336,9 @@ jobs: GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} NUM: ${{ github.event.issue.number }} FROM: ${{ steps.decide.outputs.from }} - TO: ${{ steps.decide.outputs.to }} + # Зелёное код-ревью без слияния ведёт не в S8-merged, а обратно к + # автору: метка утверждала бы, что код в dev, а его там нет. + TO: ${{ (needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' && steps.merge.outputs.merged != 'true') && 'S6-in-progress' || steps.decide.outputs.to }} run: | gh issue edit "$NUM" --repo "${{ github.repository }}" \ --add-label "$TO" --remove-label "$FROM" From d7e2c4d4f0cf2fe7d50a2cfa28d0facb4580d2d7 Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 20:30:15 +0300 Subject: [PATCH 10/13] fix: the guard says why it refused, in the issue A review label promises work. When the guard declined it wrote the reason to the run log and nothing else, so the issue sat in a status nobody was acting on and nobody could tell. #123 showed it: an outside reporter's issue was walked up to S4-spec-review, the guard refused in nine seconds because only the owner's issues enter the process, and the issue itself said not a word. Refusals that a human can act on now become a comment: wrong author, blocked, review-4. Only when a stage was actually recognised, so an unrelated label change stays silent. This is the same defect as the merge conflict that left the label untouched, seen from the other side. The pattern is worth naming: doing nothing quietly is the most expensive thing a pipeline can do. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 00525e7d..bf896138 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -77,19 +77,36 @@ jobs: -q "[.comments[] | select(.body | test(\"Вердикт:\")) | select(.body | test(\"$marker\"))] | length") fi + # Отказ обязан быть виден в issue, а не только в логе прогона. + # Ревьюшная метка обещает работу; если конвейер её не начал и промолчал, + # задача стоит в этом статусе бесконечно и никто об этом не узнаёт. + # Так и вышло на #123: чужой issue довели до S4-spec-review, guard + # отказался за 9 секунд, и в issue не было ни слова. + # + # Пишем только когда пытались запустить ревью, то есть stage опознан. + # Иначе комментарий уходил бы на каждую смену любой метки. + refuse() { + echo "$1" + gh issue comment "$NUM" --repo "${{ github.repository }}" --body \ + "Конвейер ревью не запущен: $2 + + Метка \`$LABEL\` обещает работу, которая не начнётся, поэтому статус лучше вернуть в предыдущий — иначе задача простоит здесь бесконечно. [Прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + stage="" + } + # В процесс идут только issue, созданные владельцем: репозиторий # публичный, чужие отчёты бывают невалидны и статусов не несут. if [ -z "$stage" ]; then : elif [ "$AUTHOR" != "Matysh" ]; then - echo "issue от $AUTHOR, не от владельца — пропуск" - stage="" + refuse "issue от $AUTHOR, не от владельца — пропуск" \ + "issue создан пользователем \`$AUTHOR\`, а в процесс идут только issue владельца (PROCESS.md §9). Чтобы взять задачу в работу, владельцу нужно завести свой issue со ссылкой на этот." elif [ "$BLOCKED" = "true" ]; then - echo "стоит blocked — конвейер не запускается" - stage="" + refuse "стоит blocked — конвейер не запускается" \ + "на issue стоит \`blocked\` — задача ждёт внешнего решения. Снять метку, когда решение принято." elif [ "$EXHAUSTED" = "true" ]; then - echo "стоит review-4 — решение за владельцем" - stage="" + refuse "стоит review-4 — решение за владельцем" \ + "на issue стоит \`review-4\`: лимит циклов ревью исчерпан, дальше решает владелец — разделить задачу, отклонить или арбитраж (PROCESS.md §4)." elif [ "$done_cycles" -ge "$limit" ]; then echo "циклов этапа $stage пройдено $done_cycles из $limit — лимит исчерпан" gh issue edit "$NUM" --repo "${{ github.repository }}" --add-label review-4 From 2fd042a7dea30068896f5678b4600f9a64ab58ce Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 20:41:36 +0300 Subject: [PATCH 11/13] feat: an outsider's issue is worked like any other once admitted The guard refused to review any issue the owner had not filed himself. The rule was meant to keep malformed outside reports out of the pipeline, but it checked at every step instead of at the entrance, and it duplicated a guarantee the platform already gives: only someone with write access can apply a label. Applying the first status label is the owner's explicit decision, and it is the only place the question belongs. So the author check is gone. While an issue carries no status label it sits outside the process and the invariants do not apply; once labelled, the task is in flight and who filed it stops mattering. The old rule also cost real work. On #123 an outside bug report had been analysed and specified before the guard turned it away in nine seconds, and the remedy on offer was to refile the same thing as the owner's own issue. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index bf896138..563aa9e3 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -45,7 +45,6 @@ jobs: env: GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} LABEL: ${{ github.event.label.name }} - AUTHOR: ${{ github.event.issue.user.login }} BLOCKED: ${{ contains(github.event.issue.labels.*.name, 'blocked') }} EXHAUSTED: ${{ contains(github.event.issue.labels.*.name, 'review-4') }} SMALL: ${{ contains(github.event.issue.labels.*.name, 'small') }} @@ -94,13 +93,16 @@ jobs: stage="" } - # В процесс идут только issue, созданные владельцем: репозиторий - # публичный, чужие отчёты бывают невалидны и статусов не несут. + # Автор issue здесь не проверяется (решение владельца 2026-08-13). + # Проверка стоит на входе в процесс, а не на каждом шаге: как только + # задача получила статусную метку, она в работе, и кто её завёл — не + # имеет значения. Само присвоение метки и есть явное подтверждение + # владельца, причём проверенное платформой: метки может ставить только + # тот, у кого есть право записи в репозиторий. Прежняя проверка здесь + # дублировала эту гарантию и заставляла переоформлять чужие отчёты + # своими issue — чистая работа впустую, как на #123. if [ -z "$stage" ]; then : - elif [ "$AUTHOR" != "Matysh" ]; then - refuse "issue от $AUTHOR, не от владельца — пропуск" \ - "issue создан пользователем \`$AUTHOR\`, а в процесс идут только issue владельца (PROCESS.md §9). Чтобы взять задачу в работу, владельцу нужно завести свой issue со ссылкой на этот." elif [ "$BLOCKED" = "true" ]; then refuse "стоит blocked — конвейер не запускается" \ "на issue стоит \`blocked\` — задача ждёт внешнего решения. Снять метку, когда решение принято." From be7d6b9706f045b289e1bee51c9317fe0b31e343 Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 21:05:17 +0300 Subject: [PATCH 12/13] fix: the review document is published even without a task branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issues labelled before the pipeline existed keep their spec straight in dev and have no issue/NN branch. The publish step quietly exited zero for them, so the verdict would arrive as a comment and the analysis behind it would be thrown away — the fifth instance today of a step reporting success by doing nothing. The document now goes wherever the spec itself lives: the task branch when there is one, dev otherwise. Publishing also survives dev moving on while the review ran, which takes up to forty-five minutes, by rebasing once before it gives up. Four issues are waiting on this — #12, #30, #44 and #52 — each with a spec in dev, a status label applied during the bulk pass in August and a review that never ran because nothing was there to raise the event. Issue: #114 User-Visible: no --- .github/workflows/process.yml | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 563aa9e3..b346a7c2 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -249,14 +249,21 @@ jobs: BRANCH: ${{ steps.branch.outputs.name }} NUM: ${{ github.event.issue.number }} run: | + # Ветки задачи может не быть: у задач, размеченных до появления + # конвейера, ТЗ лежит прямо в dev. Раньше шаг в этом случае молча + # выходил с нулём, и разбор ревью терялся — оставался только вердикт + # комментарием. Это тот же тихий отказ: шаг сообщал об успехе тем, что + # ничего не сделал. Документ ложится туда же, где лежит само ТЗ. + target="${BRANCH:-dev}" if [ -z "$BRANCH" ]; then - echo "ветки задачи нет — документ некуда класть"; exit 0 + echo "::warning::ветки задачи нет — документ ревью ляжет в dev" fi git checkout -- . 2>/dev/null || true git clean -fd -e docs/reviews -e node_modules >/dev/null 2>&1 || true git add docs/reviews 2>/dev/null || true if git diff --cached --quiet; then - echo "документ ревью не создан"; exit 0 + echo "::warning::документ ревью не создан" + exit 0 fi git -c user.name="claude[bot]" \ -c user.email="209825114+claude[bot]@users.noreply.github.com" \ @@ -266,9 +273,23 @@ jobs: Issue: #$NUM User-Visible: no EOF - git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \ - "HEAD:$BRANCH" - echo "документ опубликован в $BRANCH" + # Публикация в dev идёт из детачнутого состояния поверх ветки задачи + # либо dev, поэтому push нужен с явным перебазированием при гонке: + # dev мог уйти вперёд, пока шло ревью — оно длится до 45 минут. + if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \ + "HEAD:$target"; then + git fetch -q origin "$target" + if ! git -c user.name="claude[bot]" \ + -c user.email="209825114+claude[bot]@users.noreply.github.com" \ + rebase "origin/$target"; then + git rebase --abort || true + echo "::error::документ ревью не удалось опубликовать в $target: конфликт" + exit 0 + fi + git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \ + "HEAD:$target" + fi + echo "документ опубликован в $target" - name: Решение по вердикту id: decide From 9177c9a944fb13aba662f1f0cd9c8cd4c1107e37 Mon Sep 17 00:00:00 2001 From: Matysh Date: Thu, 13 Aug 2026 21:33:36 +0300 Subject: [PATCH 13/13] perf: make review scope and ceremony fit the size of the task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's report: the process works but every stage takes a long time even on simple bugs. Two causes, and neither was the one that first comes to mind. The reviewer ran everything regardless. On #89 it installed Chromium, ran all 127 smoke files and a full golden capture — right for a task rated 10/10 for complexity, absurd for a bug about a room divider. Full suites are the pre-beta gate; the review now runs typecheck, unit and build always, and smokes, golden, pytest or performance only where the diff and the AC call for them. The price of narrowing it is honesty: the reviewer must list which gates it ran, which it did not, and why, so a skipped gate is a visible decision rather than a silent one. The reviewer also built its own environment out of model turns, with no npm cache and no browser cache, paid for from the same forty-five minutes. The workflow now installs dependencies and Chromium as ordinary cached steps, after switching to the task branch so the lockfile is the branch's own. Second, ceremony did not scale down. The light track makes a spec cheap; the new trivial track does without one — S2-analysis straight to S5-ready, no spec review, AC in the issue body. It is deliberately hard to qualify for: a bug on one surface, no new UX contract, no migration, no i18n, no perf or touch effect, three checkable AC at most, and expected behaviour already on record. Nothing left to decide is the criterion that holds the whole thing up, and it cannot be met by feeling sure. Code review is never skipped on either track. It is what stands in for testing here, so it is the one stage speed may not buy. Issue: #127 Issue: #128 User-Visible: no --- .github/workflows/process.yml | 69 +++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index b346a7c2..274aaa59 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -48,6 +48,7 @@ jobs: BLOCKED: ${{ contains(github.event.issue.labels.*.name, 'blocked') }} EXHAUSTED: ${{ contains(github.event.issue.labels.*.name, 'review-4') }} SMALL: ${{ contains(github.event.issue.labels.*.name, 'small') }} + TRIVIAL: ${{ contains(github.event.issue.labels.*.name, 'trivial') }} NUM: ${{ github.event.issue.number }} run: | # Этап определяется первым: от него зависит, какие вердикты считать. @@ -58,8 +59,9 @@ jobs: *) echo "метка $LABEL конвейер не запускает" ;; esac - # Лимит циклов: 4 обычный, 2 на лёгком треке (PROCESS.md §4). - limit=4; [ "$SMALL" = "true" ] && limit=2 + # Лимит циклов: 4 обычный, 2 на лёгком и коротком треке (PROCESS.md §4). + limit=4 + if [ "$SMALL" = "true" ] || [ "$TRIVIAL" = "true" ]; then limit=2; fi # Счётчик считает вердикты ТОЛЬКО своего этапа. Раньше он брал все # подряд, и вердикт по ТЗ съедал цикл из бюджета код-ревью: на #89 @@ -134,8 +136,15 @@ jobs: fetch-depth: 0 ref: dev + # Окружение готовит workflow, а не модель своими ходами. Раньше промпт + # велел ревьюеру самому выполнить `npm ci`: минуты уходили на установку без + # кэша, платились из бюджета 45 минут и из лимитов подписки, а ходы модели + # тратились на работу инфраструктуры. В validate.yml кэш стоит на всех + # тяжёлых job, здесь его не было. - uses: actions/setup-node@v4 - with: { node-version: 22 } + with: + node-version: 22 + cache: npm # Материал ревью живёт в ветке задачи: ТЗ в docs/specs/ и код коммитятся # в issue/-slug. Если ветка запушена — переключаемся на неё, иначе @@ -156,6 +165,24 @@ jobs: echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY" fi + # Зависимости ставятся ПОСЛЕ переключения на ветку задачи: lockfile мог + # измениться именно в ней, и установка по копии из dev дала бы не то дерево. + - name: Установить зависимости + run: npm ci + + # Браузер нужен не всякому ревью (см. правило выбора гейтов в промпте), + # но когда нужен — качать его заново дороже, чем держать в кэше. + - name: Кэш браузеров Playwright + id: pw + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + - name: Установить Chromium + if: steps.pw.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + - name: Review id: review uses: anthropics/claude-code-action@v1 @@ -205,10 +232,38 @@ jobs: По каждому AC: либо он доказан автотестом и ты убедился, что тест умеет падать, либо разобран по коду с явной записью «проверено чтением, не исполнением». «Verified» без названной команды и её - результата доказательством не является. Зависимостей в рабочей - копии нет: перед гейтами выполни `npm ci`. Проверь трейлеры Issue и - User-Visible, при User-Visible: yes — правки в оба changelog в том же - коммите. + результата доказательством не является. Зависимости уже установлены + workflow, Chromium тоже — `npm ci` выполнять не нужно. Проверь + трейлеры Issue и User-Visible, при User-Visible: yes — правки в оба + changelog в том же коммите. + + **Объём гейтов соразмерен задаче.** Прогонять весь набор на каждой + правке — не тщательность, а потеря времени: полные наборы это + предрелизный гейт (PROCESS.md §8), а не гейт ревью. + + Всегда, они дешёвые: + `npx tsc --noEmit`, `npm test`, `npm run build` со сверкой трёх + копий бандла. + + По необходимости, и «необходимость» определяется diff'ом и AC: + - браузерные смоки `demo/smoke_*.mjs` — названные в AC плюс + относящиеся к тронутым поверхностям. Их 127; прогон всех уместен + только когда задача действительно задевает всё; + - `npm run golden:verify` — если diff может изменить видимый + результат: рендер, геометрия, стили, слои; + - `python -m pytest tests_backend -q` — если тронут + `custom_components/**/*.py`; + - performance-профили — если названы в AC либо тронуты + чувствительные к перфу пути. + + Дисциплина «тест должен уметь падать» не отменяется, но применяется к + тем тестам, которые ты прогонял. + + **В комментарии обязателен перечень: какие гейты прогнал, какие нет и + почему.** Это условие честности такого сужения: непрогнанный гейт + становится видимым решением, а не молчаливым пропуском. Раздел «чего + не проверял» в документе ревью — не формальность, а главный его + раздел на коротких задачах. Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь.