feat(process): the pipeline records model usage in the review document's machine block (#737)

The weekly process metrics weigh tracks and the nightly ship review in the
order quality, speed, tokens (#707), but the third axis had no source: the
claude-code-action step hides usage from the Actions log on purpose, nothing
read its execution_file, and the #728 reader printed "no data" every week.

scripts/model-usage.mjs is the single module that builds and parses the line:
`<!-- hp:usage input_tokens=N output_tokens=N cache_creation_input_tokens=N
cache_read_input_tokens=N num_turns=N -->` (sums over every model in the last
`result` message, `result.usage` when modelUsage is absent) or
`<!-- hp:usage-none reason=<code> -->`. Only the result message is read; the
rest of the file holds tool results, and no byte of it is printed.

A new step right after Review in both model_review jobs (always(),
continue-on-error) hands the line out as the job output `usage`. Usage is a
reporting figure like the stage duration, so it travels as a job output and
not through the sealed artifact: REQUIRED_FILES and the #556 gate are
unchanged. Publication treats the line as untrusted input and writes the
normalized form as the last line of the anchor block (review-doc-guard
--anchor --usage=) or right after the SHIP-REVIEW block; empty becomes
reason=missing, anything off-format reason=invalid.

The #728 reader now takes the line only from the machine block: a reviewer
quoting the previous round in prose no longer doubles its usage, and
"no data" is counted as missing, never as zero. PROCESS.md §10.4 documents
the source, the format and why it is a job output.

Issue: #737
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 12:18:13 +00:00
committed by claude[bot]
parent 707cb18f7e
commit f5a6b47b39
12 changed files with 808 additions and 28 deletions
+23
View File
@@ -1013,6 +1013,8 @@ jobs:
timeout-minutes: 45
outputs:
duration_seconds: ${{ steps.duration.outputs.seconds }}
# #737: строка расхода сессии модели (scripts/model-usage.mjs).
usage: ${{ steps.usage.outputs.line }}
steps:
- name: Начать измерение стадии
id: clock
@@ -1331,6 +1333,23 @@ jobs:
--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"},"route":{"type":"string","enum":["fix","reclassify"]},"criterion":{"type":"string"}},"required":["verdict","high","medium","summary","route"]}'
# #737: расход сессии — отчётная величина, как длительность стадии: по
# нему ничего не решается, поэтому он идёт выходом job, а не через
# запечатанный artifact (#556). Публикация разбирает строку строго. Сам
# execution_file — все сообщения сессии, с результатами инструментов —
# не выгружается и не печатается: скрипт берёт из него только `result`.
# `always()` — расход упавшей сессии хотя бы виден в сводке.
- name: Снять расход модели
id: usage
if: always()
continue-on-error: true
env:
EXEC: ${{ steps.review.outputs.execution_file }}
run: |
line=$(node scripts/model-usage.mjs --execution-file="$EXEC")
echo "line=$line" >> "$GITHUB_OUTPUT"
echo "- расход модели: \`$line\`" >> "$GITHUB_STEP_SUMMARY"
- name: Запечатать результат модели
id: result
env:
@@ -1499,6 +1518,9 @@ jobs:
# Вердикт из structured_output попадает в блок якорей (#499): по нему
# следующий заход решает, можно ли применить зелёный вердикт повторно.
OUT: ${{ steps.result.outputs.structured_output }}
# #737: расход модели — последней строкой блока якорей; строка
# недоверенная, скрипт разбирает её строго.
USAGE: ${{ needs.model_review.outputs.usage }}
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)
@@ -1561,6 +1583,7 @@ jobs:
--sha="$MATERIAL_SHA" --tree="$MATERIAL_TREE" \
--branch="${BRANCH:-dev}" --specs="$MATERIAL_SPECS" \
--issue-body="$MATERIAL_ISSUE_BODY" \
--usage="$USAGE" \
--verdict="$verdict" --high="$high" \
--route="$route" --criterion="$criterion"
else
+19
View File
@@ -125,6 +125,9 @@ jobs:
if: needs.prepare.outputs.proceed == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
# #737: строка расхода сессии модели (scripts/model-usage.mjs).
outputs:
usage: ${{ steps.usage.outputs.line }}
# Недоверенная стадия без права записи (#556): документ публикует
# `publish`. `github_token` у шага Review обязателен — без него action
# меняет OIDC на собственный App-токен с правом записи.
@@ -241,6 +244,19 @@ jobs:
--allowedTools Read,Write,Grep,Glob,Bash
--json-schema '{"type":"object","properties":{"high":{"type":"integer"},"medium":{"type":"integer"},"low":{"type":"integer"},"summary":{"type":"string"}},"required":["high","medium","low","summary"]}'
# #737: как у конвейера (_process.yml) — расход выходом job, не через
# artifact; execution_file не выгружается и не печатается.
- name: Снять расход модели
id: usage
if: always()
continue-on-error: true
env:
EXEC: ${{ steps.review.outputs.execution_file }}
run: |
line=$(node scripts/model-usage.mjs --execution-file="$EXEC")
echo "line=$line" >> "$GITHUB_OUTPUT"
echo "- расход модели: \`$line\`" >> "$GITHUB_STEP_SUMMARY"
- name: Запечатать результат модели
env:
SOURCE: ${{ runner.temp }}/ship-review.md
@@ -301,6 +317,8 @@ jobs:
MODE: ${{ needs.prepare.outputs.mode }}
PATCHES: ${{ needs.prepare.outputs.patches }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# #737: расход модели — строкой сразу после блока; недоверенный ввод.
USAGE: ${{ needs.model_review.outputs.usage }}
run: |
dir="$RUNNER_TEMP/ship-review-result"
(cd "$dir" && sha256sum -c manifest.sha256)
@@ -325,6 +343,7 @@ jobs:
issues: String(process.env.ISSUES || "").split(",").filter(Boolean).map(Number),
high: Number(process.argv[1]), medium: Number(process.argv[2]), low: Number(process.argv[3]),
runUrl: process.env.RUN_URL, mode: process.env.MODE, patches: m.parsePatches(process.env.PATCHES),
usage: process.env.USAGE ?? "",
})));
' "$high" "$medium" "$low"
} > "$DOC"