feat(process): a failed show verdict re-routes to ask without a fresh budget (#726)

A non-green show verdict that found "something to decide" went down the same
path as "fix the code": S6 with a limit of 2. Promoting the task to track:ask
was left to the agent's memory, with no named criterion and no trace, and the
exhausted budget only surfaced on the next S7 - after a fix nobody would read.

The structured verdict now carries `route` (fix | reclassify) and an optional
`criterion` (one of the six show criteria of PROCESS.md section 5). The trust
boundary reads a missing route as fix, rejects one outside the dictionary and
rejects reclassify on a green verdict. `reviewRoute` in process-track.mjs is
the single decision: on a code review of an unconfirmed show it moves the task
to track:ask and S3-spec; on an owner-confirmed show it adds `blocked` and asks
the owner; anywhere else reclassify degrades to fix with a note. The verdict
that spends the last cycle sets review-4 at once; the stage budget is shared
across tracks, so promotion changes the limit (4), not the count.

The "Решение по вердикту" step makes one `process-track.mjs route` call (from
dev, like the track step) and only executes its output: comment from a file,
labels from add/remove lists, status via status-label.mjs as before. The track
step also emits `confirmed` and a `route_note` for the review prompt; the
review document anchor gains a route tail that the old reader still parses;
wait-verdict reports the two new pipeline comments. The guard's own
spent >= limit check stays as the safety net.

Issue: #726
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 03:06:49 +00:00
committed by claude[bot]
parent 56be779ca7
commit d327ec3d93
14 changed files with 996 additions and 60 deletions
+74 -21
View File
@@ -339,6 +339,10 @@ jobs:
# для комментария слияния ship (пакетное ревью, §11.7).
risk_note: ${{ steps.track.outputs.risk_note }}
ship_risk: ${{ steps.track.outputs.ship_risk }}
# #726: подтверждение трека владельцем — решению по вердикту; заметка
# маршрута (`route`/`criterion`) — в промпт Review.
confirmed: ${{ steps.track.outputs.confirmed }}
route_note: ${{ steps.track.outputs.route_note }}
steps:
- name: Начать измерение стадии
id: clock
@@ -1171,6 +1175,8 @@ jobs:
${{ needs.prepare.outputs.risk_note }}
${{ needs.prepare.outputs.route_note }}
${{ needs.prepare.outputs.spec_body_changed == 'true' && format('ТЗ в теле issue менялось после зелёного ревью ТЗ ({0}, записанный хеш {1}). GitHub хранит правки тела без diff — дельту показать нельзя, поэтому AC сверяются с ТЕКУЩИМ текстом целиком, а не по дельте, и находка называется в вердикте (#517).', needs.prepare.outputs.spec_body_doc, needs.prepare.outputs.spec_body_recorded) || '' }}
Правила ревью в этом промпте не повторяются (#634): их канон —
@@ -1322,7 +1328,7 @@ jobs:
claude_args: |
--max-turns 150
--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"]}'
--json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["green","yellow","red"]},"high":{"type":"integer"},"medium":{"type":"integer"},"summary":{"type":"string"},"route":{"type":"string","enum":["fix","reclassify"]},"criterion":{"type":"string"}},"required":["verdict","high","medium","summary","route"]}'
- name: Запечатать результат модели
id: result
@@ -1495,6 +1501,9 @@ jobs:
run: |
verdict=$(printf '%s' "$OUT" | jq -r '.verdict // empty' 2>/dev/null || true)
high=$(printf '%s' "$OUT" | jq -r '.high // empty' 2>/dev/null || true)
# #726: маршрут вердикта — хвостом строки якоря; вне словаря скрипт его не пишет.
route=$(printf '%s' "$OUT" | jq -r '.route // empty' 2>/dev/null || true)
criterion=$(printf '%s' "$OUT" | jq -r '.criterion // empty' 2>/dev/null || true)
# Ветки задачи может не быть: у задач, размеченных до появления
# конвейера, ТЗ лежит прямо в dev. Раньше шаг в этом случае молча
# выходил с нулём, и разбор ревью терялся — оставался только вердикт
@@ -1551,7 +1560,8 @@ jobs:
--sha="$MATERIAL_SHA" --tree="$MATERIAL_TREE" \
--branch="${BRANCH:-dev}" --specs="$MATERIAL_SPECS" \
--issue-body="$MATERIAL_ISSUE_BODY" \
--verdict="$verdict" --high="$high"
--verdict="$verdict" --high="$high" \
--route="$route" --criterion="$criterion"
else
echo "::warning::$SOURCE не найден — документа для публикации нет"
fi
@@ -1707,7 +1717,26 @@ jobs:
VALIDATE_URL: ${{ needs.prepare.outputs.validate_url }}
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
NUM: ${{ github.event.issue.number }}
# #726: маршрут вердикта — трек и его подтверждение владельцем (шаг
# трека), бюджет этапа (guard), ветка материала с документами ревью.
TRACK: ${{ needs.prepare.outputs.track }}
CONFIRMED: ${{ needs.prepare.outputs.confirmed }}
LABELS: ${{ needs.guard.outputs.labels }}
SPENT: ${{ needs.guard.outputs.spent }}
LIMIT: ${{ needs.guard.outputs.limit }}
CYCLE: ${{ needs.guard.outputs.cycle }}
BRANCH: ${{ needs.prepare.outputs.branch }}
run: |
# ship и повторно применённый зелёный (#499) модель не вызывали:
# маршрута у них нет, решение прежнее — зелёный двигает вперёд.
green_forward() {
green=true
case "$STAGE" in
spec) from=S4-spec-review; to=S5-ready ;;
code) from=S7-code-review; to=S8-merged ;;
esac
}
green=""; from=""; to=""
# Повторно применимый зелёный вердикт (#499) главнее ship: код уже
# прочитан моделью, и пакетному ревью перечитывать его незачем.
if [ "$SHIP" = "true" ] && [ "$REUSE" != "true" ]; then
@@ -1725,6 +1754,7 @@ jobs:
<!-- hp:ship-merge material=$MATERIAL -->
EOF
gh issue comment "$NUM" --repo "${{ github.repository }}" --body-file /tmp/ship-merge.md
green_forward
elif [ "$REUSE" = "true" ]; then
# Модель не вызывалась: вердикт — записанный конвейером зелёный
# прошлого захода, дерево вне docs/reviews с тех пор не менялось (#499).
@@ -1734,28 +1764,51 @@ jobs:
"Вердикт: зелёный · заход r${{ needs.guard.outputs.cycle }} · применён повторно из r${REUSE_ROUND} без вызова модели (#499) · High: 0 · Medium: 0 · Документ: docs/reviews/${REUSE_DOC}
Дерево материала \`${short_tree}\` с захода r${REUSE_ROUND} не изменилось ни в одном файле вне \`docs/reviews/\` (проверено \`git diff\` по содержимому). Новый документ не публикуется: разбирать нечего. Любое отличие дерева — ребейз, тест, фикстура, ТЗ — запустило бы полный разбор."
green_forward
else
verdict=$(echo "$OUT" | jq -r '.verdict')
high=$(echo "$OUT" | jq -r '.high')
# #726: вердикт модели решает скрипт трека командой route — ОДИН вызов:
# статус, метки трека, `blocked` и `review-4`, тело комментария
# файлом и строка сводки. Вперёд двигает только зелёный (§7.2);
# жёлтый и красный — цикл и возврат автору, на код-ревью show с
# `route: reclassify` — повышение до ask или вопрос владельцу.
# Bash только исполняет: комментарий — из файла скрипта, метки —
# по его выходу. Скрипт — из dev, как у шага трека: рабочая копия
# здесь — ветка задачи после публикации, и ветка show/ship,
# отставшая от dev, его может ещё не нести. Метки — текущие: шаг
# трека мог в этом же прогоне сменить ship на show.
git fetch -q origin dev
tools="$RUNNER_TEMP/route-tools"
rm -rf "$tools" && mkdir -p "$tools"
git archive origin/dev scripts | tar -x -C "$tools"
printf '%s' "$OUT" > "$RUNNER_TEMP/route-verdict.json"
labels=$(gh issue view "$NUM" --repo "${{ github.repository }}" --json labels \
--jq '[.labels[].name] | join(",")') || labels="$LABELS"
route=$(node "$tools/scripts/process-track.mjs" route --stage="$STAGE" --track="$TRACK" \
--confirmed="$CONFIRMED" --labels="$labels" --verdict="$RUNNER_TEMP/route-verdict.json" \
--spent="$SPENT" --limit="$LIMIT" --num="$NUM" --cycle="$CYCLE" --branch="$BRANCH" \
--out="$RUNNER_TEMP/route" \
--run-url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}")
printf '%s\n' "$route"
field() { printf '%s\n' "$route" | sed -n "s/^$1=//p"; }
verdict=$(field verdict); high=$(field high)
green=$(field green); from=$(field from); to=$(field to)
add=$(field add_labels); remove=$(field remove_labels); comment=$(field comment)
if [ -n "$comment" ]; then
gh issue comment "$NUM" --repo "${{ github.repository }}" --body-file "$comment"
fi
edit=()
if [ -n "$add" ]; then edit+=(--add-label "$add"); fi
if [ -n "$remove" ]; then edit+=(--remove-label "$remove"); fi
if [ "${#edit[@]}" -gt 0 ]; then
gh issue edit "$NUM" --repo "${{ github.repository }}" "${edit[@]}"
fi
fi
echo "вердикт: $verdict, High: $high"
# Вперёд двигает ТОЛЬКО зелёный. Жёлтый и красный возвращают
# автору: на прогоне #111 жёлтый означал, что AC описывает неверное
# изменение контракта — реализовать такое ТЗ значит сделать ошибку
# по инструкции. Оба считаются циклом.
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
# Перехода нет — сбой шага, а не зелёный: метка остаётся, зовётся владелец.
case "$green" in true|false) ;; *) echo "::error::решение по вердикту не дало исхода (green=$green)"; exit 1 ;; esac
if [ -z "$from" ] || [ -z "$to" ]; then
echo "::error::решение по вердикту не дало перехода ($from -> $to)"
exit 1
fi
echo "green=$green" >> "$GITHUB_OUTPUT"
echo "from=$from" >> "$GITHUB_OUTPUT"