From a5d77eb8170d03a6fdc623f215c324aab933adf7 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Sat, 22 Aug 2026 16:31:56 +0000 Subject: [PATCH] chore(ci): migrate autofix prose to the design record and ratchet growth (#9677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(ci): migrate autofix prose to the design record and ratchet growth `qwen-autofix.yml` was at 462,720 bytes — 90% of GitHub's 500 KB start-runs limit and 7,280 under this repo's gate. The prose migration in #9517 had regained 78 KB; a single feature commit gave 25 KB of it back two days later, 53% of it comment lines that belonged in the design record. Nothing objected, because the gate is a ceiling and only speaks when a file is already at the wall. Two changes, addressing the level and the slope. Level: 76 comment blocks of 8+ lines move into qwen-autofix.md as af-073..af-148, each leaving its opening two lines plus the usual pointer. The file drops to 419,995 — 50 KB under the gate. Slope: every workflow's recorded size now lives in .github/workflows/.size-baseline, and check-workflow-size.sh fails a file that exceeds its entry by more than 4 KB. Growing a file is still allowed; the ratchet only insists the growth be visible in review instead of discovered at the wall. A file well under its baseline warns so the slack is reclaimed rather than banked. Two things the migration had to learn, both caught by contract tests rather than by inspection: Identical prose shares one af id. A step inlined into several jobs must stay byte-identical across copies, and minting a separate id per copy broke that for the git-config sanitize step — same length, different pointer digits. A cross-file editing contract is not prose. "This copy and the one in qwen-triage must be edited together" only does its job where the editor will see it; moving it to the design record is how a lockstep silently breaks. Blocks naming a sibling workflow or script, or spelling the contract out, stay put. Behaviour is unchanged and checked, not assumed: both YAML documents were parsed and compared with comment lines dropped from every string, and they are equal. * fix(ci): harden the workflow-size ratchet per review - key the vitest baseline lookups by separator-agnostic file name so the merge-queue Windows lane resolves entries (split('/') missed win32 joins) - fail closed on malformed .size-baseline lines: non-numeric values, leading zeros (bash octal), and extra fields no longer disable or mis-key the ratchet; keep an unterminated final line - execute the gate script end-to-end in tests (growth, missing entry, missing baseline, slack warning, malformed line) so its decision branches are witnessed - correct af-079/af-123/af-084 attributions in the design record * fix(ci): pin ratchet boundaries and skip pre-bash-4 test hosts * fix(ci): end migrated autofix teasers at sentence boundaries per review --------- Co-authored-by: qwen-code-dev-bot Co-authored-by: qwen-code-dev-bot Co-authored-by: Qwen-Coder --- .github/scripts/autofix-push-and-report.sh | 63 +- .github/scripts/check-workflow-size.sh | 51 +- .github/workflows/.size-baseline | 66 + .github/workflows/qwen-autofix.md | 1531 ++++++++++++++++++++ .github/workflows/qwen-autofix.yml | 695 ++------- scripts/tests/workflow-size.test.js | 269 +++- 6 files changed, 2064 insertions(+), 611 deletions(-) create mode 100644 .github/workflows/.size-baseline diff --git a/.github/scripts/autofix-push-and-report.sh b/.github/scripts/autofix-push-and-report.sh index 4872244ea4..ff75361be8 100755 --- a/.github/scripts/autofix-push-and-report.sh +++ b/.github/scripts/autofix-push-and-report.sh @@ -2,10 +2,12 @@ # Push the round's commit to the PR head and post the round report. # # The body below is the 'Push and report' step of review-address in -# .github/workflows/qwen-autofix.yml, byte-identical to the inline block it -# came from: 626 lines, ~41 KB. The file it left is within a few KB of the -# repo's 470,000-byte gate, and GitHub stops starting runs past 512,000 without -# saying so (.github/scripts/check-workflow-size.sh). No absolute size is +# .github/workflows/qwen-autofix.yml — the inline block it came from (626 +# lines, ~41 KB at the move), its long comments since migrated to +# qwen-autofix.md pointers like the rest of the workflow. The file it left +# is within a few KB of the repo's 470,000-byte gate, and GitHub stops starting +# runs past 512,000 without saying so (.github/scripts/check-workflow-size.sh). +# No absolute size is # quoted here on purpose — main moves it every day, and a number that decays is # how this comment earned three review rounds. It is also # the step docs/design/autofix-gate-runner-isolation.md moves into its own @@ -78,15 +80,8 @@ MODEL_DISPLAY="${MODEL:-default}" # Growth-audit trail (+ re-arm on sound): audit rounds record the # verdict under the key the baseline was READ under — same rule as # the growth markers, same dead-key hazard (a supersede-exempt -# round can report under a stale WINDOW after a re-arm). The -# verdict comes from AUDIT_VERDICT — the verdict the verification -# GATE validated and surfaced as a step output — NOT a re-read of -# growth-audit.json: the branch's own build/tests run as the runner -# user and WORKDIR is a predictable path they can write, so the -# file could change after the gate looked. Re-arming is allowed -# for completed rounds only ($1 = allow): a sound verdict whose -# round then FAILED must not re-anchor the window — the failure -# path re-measures under the same window instead. +# round can report under a stale WINDOW after a re-arm). +# Full rationale → qwen-autofix.md#af-131 emit_growth_audit_marker() { local allow_rearm="${1:-false}" [[ "${KISS_AUDIT}" == 'true' ]] || return 0 @@ -233,12 +228,7 @@ resolve_and_reply_threads() { fi # The mirror of the resolve above: a finding the agent did NOT # resolve keeps its thread open, and this answers it IN that thread. - # Without it the reason sits only in the round summary, so the - # reviewer who opens the still-open thread sees silence and cannot - # tell their finding was read. Same neutralisation as the summary - # body — a reply is model output posted verbatim under the bot - # identity, so it could otherwise smuggle a forged control marker. - # Best-effort: a reply failure must never fail a good push. + # Full rationale → qwen-autofix.md#af-132 if [[ -s "${WORKDIR}/comment-replies.json" ]] && jq -e 'type == "array"' "${WORKDIR}/comment-replies.json" > /dev/null 2>&1; then REPLIED_N=0 @@ -271,12 +261,8 @@ resolve_and_reply_threads() { # later round whose agent rewrites an unchanged declination # must not post the same bot reply twice on one thread # (observed 2026-08-16: an identical reply posted three - # times, #9296). Skip when the thread already carries a - # comment by the bot whose body EQUALS the neutralised body - # about to be posted; a changed body — a new reason in a - # later round — still posts. Best-effort like the rest: with - # a stale or empty threads view this degrades to the old - # post-always behavior. + # times, #9296). + # Full rationale → qwen-autofix.md#af-133 if jq -e --argjson id "${root_id}" --arg bot "${AUTOFIX_BOT}" \ --arg body "${REPLY_BODY}" ' map(select(any(.comments.nodes[]; .databaseId == $id))) @@ -382,13 +368,8 @@ bash "${RUNNER_TEMP}/resanitize-git-config.sh" if [[ "${OUTCOME}" == "fixed" ]]; then NEXT_ROUND="$(( ROUND + 1 ))" # The tree the gate verified is what gets pushed: assert HEAD is - # the gate's verified_head before touching credentials. A repo - # redirect (a planted .git/commondir/GIT_DIR — the first defused - # by resanitize, the second by the env strip) would otherwise let - # `git rev-parse HEAD` and the push read an attacker repo whose - # HEAD differs; this compares against the value the gate recorded - # in GITHUB_OUTPUT (unreachable from a disk write). Empty - # verified_head only on a noop, which does not reach this push. + # the gate's verified_head before touching credentials. + # Full rationale → qwen-autofix.md#af-134 HEAD_NOW="$(git rev-parse HEAD)" if [[ -z "${VERIFIED_HEAD}" || "${HEAD_NOW}" != "${VERIFIED_HEAD}" ]]; then echo "::error::HEAD ${HEAD_NOW} is not the gate's verified head ${VERIFIED_HEAD:-} — refusing to push" @@ -560,13 +541,8 @@ fi # Bounded retry on the report post: this one comment carries the # round's ENTIRE persisted state (autofix-eval watermark/round, -# redcheck head, growth baseline). The push has already landed, so -# a transient API failure here loses the marker while keeping the -# growth — the retry scan would re-anchor the baseline at the -# post-push size and re-evaluate feedback it already addressed. -# Three attempts bound that to genuine outages; the final failure -# keeps today's semantics (step fails, no marker, next scan -# retries the round). +# redcheck head, growth baseline). +# Full rationale → qwen-autofix.md#af-135 REPORT_POSTED='false' for attempt in 1 2 3; do if gh pr comment "${PR}" --repo "${REPO}" --body-file "${WORKDIR}/report.md"; then @@ -590,13 +566,8 @@ if [[ "${OUTCOME}" == "fixed" && "${MAX_ROUNDS}" == "${TAKEOVER_MAX_ROUNDS}" ]] # Crossing trigger, not an equality test: failure rounds also # advance the round counter, so `push@9, crash@10, push@11` # would skip an exact %10 check forever — and a failure-heavy - # PR is the very PR the digest exists for. Post on the first - # PUSHED round once 10+ rounds have accumulated since the last - # digest in THIS window (or since the window opened). The - # window opens at the round SEED, not at zero: a '/takeover - # from 60' counter starts at 60, so the no-digest-yet baseline - # is the seed — otherwise the seed-inflated counter digests on - # the window's first push with a 1-2 round census. + # PR is the very PR the digest exists for. + # Full rationale → qwen-autofix.md#af-136 MS_LAST="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg win "${WINDOW:-none}" --argjson start "${ROUND_START:-0}" ' [ .[] | select((.user.login // "") == $ab) | (.body // "") | [ scan("") ] | .[] diff --git a/.github/scripts/check-workflow-size.sh b/.github/scripts/check-workflow-size.sh index ed60a58289..5ce28db076 100755 --- a/.github/scripts/check-workflow-size.sh +++ b/.github/scripts/check-workflow-size.sh @@ -16,7 +16,44 @@ GITHUB_LIMIT_BYTES=512000 GATE_BYTES="${WORKFLOW_SIZE_GATE_BYTES:-470000}" WARN_BYTES=$((GATE_BYTES - 25000)) +# The gate above is the ceiling; the baseline below is the RATCHET. The gate +# alone only objects once a file is nearly at the wall, so growth accumulates +# invisibly until one unlucky PR has to pay for everyone: qwen-autofix.yml +# regained 78 KB when its prose moved out (#9517) and gave 25 KB of it back in +# a single feature commit two days later, unremarked. Each file's recorded size +# lives in .size-baseline; exceeding it by more than the allowance fails until +# the number is updated in the same PR, which turns the drift into one line a +# reviewer sees. +BASELINE_FILE='.github/workflows/.size-baseline' +GROWTH_ALLOWANCE="${WORKFLOW_SIZE_GROWTH_ALLOWANCE:-4096}" +# Loose enough that ordinary edits do not churn the manifest, tight enough that +# a file which shed real weight gets its baseline reclaimed rather than banking +# the slack for the next unreviewed 25 KB. +SLACK_BYTES=20000 + status=0 +declare -A baseline=() +if [[ -r "${BASELINE_FILE}" ]]; then + # The || clause keeps an unterminated final line, which read reports as a + # failure and the loop would otherwise silently drop. + while read -r recorded name extra || [[ -n "${recorded}" ]]; do + [[ -z "${recorded}" || "${recorded}" == \#* ]] && continue + # Fail closed on malformed lines: bash evaluates a leading-zero value as + # OCTAL at the arithmetic sites below, a non-numeric one errors both + # comparisons to false (the ratchet would fail OPEN), and extra fields + # key differently in the vitest mirror. + if [[ -z "${name}" || -n "${extra}" || ! "${recorded}" =~ ^(0|[1-9][0-9]*)$ ]]; then + echo "::error file=${BASELINE_FILE}::${BASELINE_FILE} entry '${recorded}${name:+ ${name}}${extra:+ ${extra}}' is malformed — expected exactly ' ' with a decimal byte count (no leading zeros)" + status=1 + continue + fi + baseline["${name}"]="${recorded}" + done <"${BASELINE_FILE}" +else + echo "::error::${BASELINE_FILE} is missing or unreadable — the growth ratchet cannot run" + exit 1 +fi + shopt -s nullglob for file in .github/workflows/*.yml .github/workflows/*.yaml; do if ! size="$(wc -c <"${file}")"; then @@ -24,6 +61,7 @@ for file in .github/workflows/*.yml .github/workflows/*.yaml; do status=1 continue fi + size="${size// /}" pct=$((size * 100 / GITHUB_LIMIT_BYTES)) if ((size > GATE_BYTES)); then echo "::error file=${file}::${file} is ${size} bytes — ${pct}% of GitHub's ${GITHUB_LIMIT_BYTES}-byte start-runs limit, past this repo's ${GATE_BYTES}-byte gate. Move prose into a sibling .md and long steps into .github/scripts/; do not raise the gate." @@ -31,9 +69,20 @@ for file in .github/workflows/*.yml .github/workflows/*.yaml; do elif ((size > WARN_BYTES)); then echo "::warning file=${file}::${file} is ${size} bytes (${pct}% of GitHub's limit) — approaching the ${GATE_BYTES}-byte gate." fi + + base="${baseline[${file##*/}]:-}" + if [[ -z "${base}" ]]; then + echo "::error file=${file}::${file} has no entry in ${BASELINE_FILE}. Add '${size} ${file##*/}' so its growth is tracked." + status=1 + elif ((size > base + GROWTH_ALLOWANCE)); then + echo "::error file=${file}::${file} grew to ${size} bytes, $((size - base)) over its recorded ${base} (allowance ${GROWTH_ALLOWANCE}). Move prose into a sibling .md and long steps into .github/scripts/ — or, if the growth is real, update ${BASELINE_FILE} in this PR and say why." + status=1 + elif ((size + SLACK_BYTES < base)); then + echo "::warning file=${file}::${file} is ${size} bytes, $((base - size)) under its recorded ${base} — lower the entry in ${BASELINE_FILE} so the slack is not banked." + fi done if ((status == 0)); then - echo "✅ every workflow file is under the ${GATE_BYTES}-byte gate" + echo "✅ every workflow file is under the ${GATE_BYTES}-byte gate and within ${GROWTH_ALLOWANCE} bytes of its recorded baseline" fi exit "${status}" diff --git a/.github/workflows/.size-baseline b/.github/workflows/.size-baseline new file mode 100644 index 0000000000..d105e4317c --- /dev/null +++ b/.github/workflows/.size-baseline @@ -0,0 +1,66 @@ +# Recorded byte size of each workflow file, enforced by +# .github/scripts/check-workflow-size.sh. +# +# The absolute gate in that script is the ceiling; this file is the ratchet. +# A workflow may not grow past its recorded size by more than the allowance +# without updating the number here IN THE SAME PR — which is the point: it +# turns invisible drift into one reviewed line. qwen-autofix.yml regained +# 78 KB when its prose moved to qwen-autofix.md (#9517), then gave 25 KB of +# it back in a single feature commit two days later, with nothing objecting +# until the next PR ran out of ceiling. +# +# Growing a file is allowed — deliberately. Prefer moving prose into the +# sibling .md and long steps into .github/scripts/ first; if the growth is +# real, bump the number and say why in the PR. +2226 assign-issue-owner.yml +3480 audio-capture-prebuilds.yml +9023 auto-minimize-spam.yml +4638 build-and-publish-image.yml +29715 cd-cua-driver.yml +2076 cd-mobile-mcp.yml +69782 ci.yml +1482 codeql.yml +9389 comment-attachment-guard.yml +31677 desktop-release.yml +2038 docs-page-action.yml +10005 dsw-swe-verified-release.yml +12340 e2e.yml +11394 finalize-release.yml +15871 live-host-release.yml +6384 live-host.yml +7642 main-ci-failure-issue.yml +1686 npm-cache.yml +7299 pr-force-push-reminder.yml +6495 pr-self-report-label.yml +9646 qwen-autofix-fork-bridge.yml +5942 qwen-autofix-fork-signal.yml +392111 qwen-autofix.yml +7061 qwen-ci-flaky-rerun.yml +151937 qwen-code-pr-review.yml +79041 qwen-fleet-shepherd.yml +20525 qwen-issue-followup-bot.yml +5760 qwen-pr-safety-precheck.yml +27648 qwen-triage-finalize.yml +344548 qwen-triage.yml +9657 release-sdk-java.yml +22037 release-sdk-python.yml +19094 release-sdk.yml +14546 release-vscode-companion.yml +39291 release.yml +43717 repo-hygiene.yml +1079 scorecard-monthly.yml +10691 sdk-java.yml +3886 sdk-python.yml +3197 security-checks.yml +6777 serve-ab-publish.yml +17013 serve-ab.yml +2641 stale.yml +8653 sync-cua-driver-to-oss.yml +10920 sync-desktop-to-oss.yml +10018 sync-live-host-to-oss.yml +10138 sync-release-to-oss.yml +3303 update-ecs-runner-qwen.yml +2307 web-shell-visuals-cleanup.yml +15380 web-shell-visuals-publish.yml +16384 web-shell-visuals.yml +4712 windows-runner-smoke.yml diff --git a/.github/workflows/qwen-autofix.md b/.github/workflows/qwen-autofix.md index a16f27899e..9846d3ee2b 100644 --- a/.github/workflows/qwen-autofix.md +++ b/.github/workflows/qwen-autofix.md @@ -30,6 +30,16 @@ So: **prose belongs here, long steps belong in `.github/scripts/`.** `.github/scripts/check-workflow-size.sh` fails CI before the limit can be reached again. +That gate is a ceiling, and a ceiling only objects once a file is nearly at the +wall — so growth accumulates unremarked until one unlucky PR has to pay for +everyone. This file regained 78 KB in that migration and gave 25 KB of it back +in a single feature commit two days later, with nothing raising a hand. The +same script therefore also enforces a **ratchet**: every workflow's recorded +size lives in `.size-baseline`, and exceeding it by more than the allowance +fails until the number is updated in the same PR. Growing a file is still +allowed — the ratchet only insists the growth be visible in review rather than +discovered at the wall. + ### Steps that moved out, not just their prose `review-address` · `Push and report` was 626 lines of inline shell — ~41 KB, @@ -155,6 +165,80 @@ task-oriented guides — what a maintainer types and what happens next — see: - [71. review-address · Report dry-run / failure — Bilingual companion. Repo convention is English first, Chinese in a collapsed
.…](#af-071) - [72. review-address · Report dry-run / failure — Flip the status comment out of "working" so a finished round never leaves a live-looking…](#af-072) - [73. review-address · Report dry-run / failure — Idle (silent-sandbox) timeouts are EXCLUDED from the cumulative timeout cap.…](#af-073) +- [74. run — Per-author tail budget inside Critical-only mode. An account is an…](#af-074) +- [75. run — Growth audit: a budget breach engages Critical-only AND makes the round a…](#af-075) +- [76. run — Failed-check annotation patterns that mean the INFRASTRUCTURE died, not the code…](#af-076) +- [77. run — Upper bound on review targets emitted per scan (fan-out defense-in-depth; excess…](#af-077) +- [78. run — Upper bound on candidates INSPECTED per scan: idle candidates consume serial API…](#af-078) +- [79. run — Commit-status context stamped PENDING on a PR head when a scan dispatches a…](#af-079) +- [80. run — Consecutive-failure sub-cap, distinct from the total round cap (TAKEOVER_MAX_ROUNDS,…](#af-080) +- [81. run — Cumulative agent-timeout sub-cap, the sibling of the consecutive cap for the…](#af-081) +- [82. route · Decide phases — Real-time review triggers: process the SAME managed set the scheduled scan does,…](#af-082) +- [83. route · Decide phases — Comment-command sugar over the labels: TAKEOVER_COMMAND applies TAKEOVER_LABEL,…](#af-083) +- [84. route · Decide phases — The bot only applies this label from takeover-command, which posts the engage…](#af-084) +- [85. issue-autofix · Sanitize workspace git config — Rather than denylist each exec-vector family (which kept missing new ones), KEEP…](#af-085) +- [86. issue-autofix · Stage trusted schema gate — The staged copy's trusted-base provenance holds at cp time only: RUNNER_TEMP is…](#af-086) +- [87. issue-autofix · Verification gate — Settings-schema freshness gate, shared with the triage-and-address verify step…](#af-087) +- [88. issue-autofix · Withdraw claim on failure — Same hygiene as the PR-lane DETAIL_FILE excerpt: -c drops a partial multi-byte…](#af-088) +- [89. takeover-command · Toggle takeover label — Ack HERE, not via the pull_request:labeled round-trip: that event has been…](#af-089) +- [90. takeover-command · Toggle takeover label — Release ack, direct from the command — the exact mirror of the engage side…](#af-090) +- [91. retry-command · Post re-arm marker — Management resumed — the escalation label is stale. 404 is the common case (the…](#af-091) +- [92. takeover-ack · Acknowledge takeover state change — R2-4 mirror for the engaged direction: a delayed engaged ack — a red run re-run…](#af-092) +- [93. takeover-ack · Acknowledge takeover state change — The escalation label goes stale on a real engage or any release (a human is…](#af-093) +- [94. review-scan · Scan for PRs with new feedback — Every lane that reaches this scan is supposed to hold the PAT: route now…](#af-094) +- [95. review-scan · Scan for PRs with new feedback — Candidate PRs: open, same-repo, targeting main, and either authored by the…](#af-095) +- [96. review-scan · Scan for PRs with new feedback — Same admission as the scheduled scan below. In-repo PRs fail CLOSED on a missing…](#af-096) +- [97. review-scan · Scan for PRs with new feedback — Skip-labeled PRs are excluded HERE, not only at the address gate: that gate…](#af-097) +- [98. review-scan · Scan for PRs with new feedback — Dispatch-pending marker: a scan that dispatched this PR within…](#af-098) +- [99. review-scan · Scan for PRs with new feedback — Delay-window fallback: a review run parked BEFORE its job starts (the 10-minute…](#af-099) +- [100. review-scan · Scan for PRs with new feedback — Auto-rerun a check that died on INFRASTRUCTURE, not the code (see…](#af-100) +- [101. review-scan · Scan for PRs with new feedback — startedAt is the only staleness clock: a check blocks only if it started within…](#af-101) +- [102. review-scan · Scan for PRs with new feedback — Ack-on-defer (#8888): a real-time human review routed this scan straight here,…](#af-102) +- [103. review-scan · Scan for PRs with new feedback — Pre-first-eval floor: the PR's IMMUTABLE creation time. Feedback cannot predate…](#af-103) +- [104. review-scan · Scan for PRs with new feedback — ROUND counting is windowed by KEY EQUALITY, not timestamps: the current window…](#af-104) +- [105. review-scan · Scan for PRs with new feedback — Seed for THIS window, from the ' from N' marker carried by the comment that…](#af-105) +- [106. review-scan · Scan for PRs with new feedback — Consent may have moved since PR_META: skip wins everywhere, and a takeover…](#af-106) +- [107. review-scan · Scan for PRs with new feedback — The escalation label rides EVERY cap detection, noticed or not: the…](#af-107) +- [108. review-scan · Scan for PRs with new feedback — Conflict-park gate for the loop's OWN head move: while a conflict handoff pends…](#af-108) +- [109. review-scan · Scan for PRs with new feedback — Auto-update a PR that is red ONLY because of a stale base (see the…](#af-109) +- [110. review-scan · Scan for PRs with new feedback — STALE_BASE_REDS is pure jq over data already in memory (CHECKS_JSON,…](#af-110) +- [111. review-address · Stage trusted schema gate and agent runner — The staged copies' trusted-base provenance holds at cp time only: RUNNER_TEMP is…](#af-111) +- [112. review-address · Prepare branch and feedback — This PAT-bearing step runs git (status/restore/fetch/checkout and a push…](#af-112) +- [113. review-address · Prepare branch and feedback — ---- address-time eligibility recheck --------------------------- Fan-out can…](#af-113) +- [114. review-address · Prepare branch and feedback — Maintainer-fork target: the branch does not exist on origin — fetch it (data…](#af-114) +- [115. review-address · Prepare branch and feedback — Allow-edits pushes ride the classic-PAT grant — GITHUB_TOKEN and fine-grained…](#af-115) +- [116. review-address · Prepare branch and feedback — Release the dispatch-pending marker the emitting scan stamped on this head: the…](#af-116) +- [117. review-address · Prepare branch and feedback — Mechanical churn must not burn the budget: one dependency bump rewrites hundreds…](#af-117) +- [118. review-address · Prepare branch and feedback — The instant THIS round's net was measured. Stamped into the growth-now marker so…](#af-118) +- [119. review-address · Prepare branch and feedback — NOTE (#9114 R2-8/R6-3): re-anchoring on an EXTERNAL head move (an author push)…](#af-119) +- [120. review-address · Prepare branch and feedback — KNOWN RESIDUAL (#9114): this sibling read still filters on the comment's…](#af-120) +- [121. review-address · Prepare branch and feedback — Growth audit: a budget breach engages Critical-only AND makes the round a…](#af-121) +- [122. review-address · Prepare branch and feedback — Conflict-handoff idempotence: a conflict verdict parks the PR at a genuinely…](#af-122) +- [123. review-address · Prepare branch and feedback — Growth audit (a size signal triggers a JUDGMENT, never a stop): the window is…](#af-123) +- [124. review-address · Post autofix status comment — The agent below runs for up to 130 minutes and the verification gate adds more,…](#af-124) +- [125. review-address · Triage and address — The primary attempt's real budget: 120m, with a 10-minute margin under the…](#af-125) +- [126. review-address · Triage and address — Prepare severed hooks for its PAT-bearing git ops; THIS step holds no PAT, so…](#af-126) +- [127. review-address · Repair deterministic rejection — Which side is corrupt is NOT known here — jq -s fails if EITHER input is…](#af-127) +- [128. review-address · Finalize verification — The verdict travels WITH the attempt whose outcome is selected: a repair pass…](#af-128) +- [129. review-address · Finalize verification — Conclusion gate: fixed/noop are the ONLY outcomes that release the PAT push. A…](#af-129) +- [130. review-address · Finalize verification — handoff and the two brake-violation rejections are deliberate, PUBLISHED…](#af-130) +- [131. review-address · Push and report — Growth-audit trail (+ re-arm on sound): audit rounds record the verdict under…](#af-131) +- [132. review-address · Push and report — The mirror of the resolve above: a finding the agent did NOT resolve keeps its…](#af-132) +- [133. review-address · Push and report — Idempotence gate: a crash-and-rerun of this round, a same-run repair that…](#af-133) +- [134. review-address · Push and report — The tree the gate verified is what gets pushed: assert HEAD is the gate's…](#af-134) +- [135. review-address · Push and report — Bounded retry on the report post: this one comment carries the round's ENTIRE…](#af-135) +- [136. review-address · Push and report — Crossing trigger, not an equality test: failure rounds also advance the round…](#af-136) +- [137. review-address · Report dry-run / failure — This step also posts a round report (timeout / gate-rejection / abort), so it…](#af-137) +- [138. review-address · Report dry-run / failure — handoff rounds end with a SUCCESS job status (a deliberate verdict), so they…](#af-138) +- [139. review-address · Report dry-run / failure — Cause-aware wording, most specific first — a model error and a gate crash each…](#af-139) +- [140. review-address · Report dry-run / failure — A deliberate stop, not a failed fix: the agent stopped under instruction and…](#af-140) +- [141. review-address · Report dry-run / failure — A brake VIOLATION, not a failed fix: the agent stopped under instruction but…](#af-141) +- [142. review-address · Report dry-run / failure — The committed sibling of the dirty-handoff violation: the round HAS a commit…](#af-142) +- [143. review-address · Report dry-run / failure — A conflict round must PARK quietly at the human call: its own stale-base merge…](#af-143) +- [144. review-address · Report dry-run / failure — Prepare RAN (outcome success/failure) but produced no feedback to read — prepare…](#af-144) +- [145. review-address · Report dry-run / failure — CUMULATIVE timeout breaker — the sibling of the consecutive one above, for the…](#af-145) +- [146. review-address · Report dry-run / failure — The agent committed (verify recorded committed=true before any gate could fail),…](#af-146) +- [147. review-address · Report dry-run / failure — Same byte-budget hygiene as the English excerpt above. 3000 bytes ≈ 1000 CJK…](#af-147) --- @@ -2186,3 +2270,1450 @@ budget remedy again — and an idle round never exhausted any budget, so it must not steer the narrowing. Idle rounds are excluded there with the same needle the cap census uses. ``` + + + +### 74. run — Per-author tail budget inside Critical-only mode. An account is an… + +In `run`. + +```text +Per-author tail budget inside Critical-only mode. An account is an +ACCOUNTABILITY unit, not a throttle: a human login can host an automated +reviewer loop with the exact regeneration property the review bot has +(feedback re-generated after every push, at zero marginal cost). So the +brake keys on measured regeneration, not identity: every source gets a +bounded number of untagged feedback batches per counting window once +Critical-only engages — the review bot's budget is zero (all deferred), +a human's is this many CONSUMED batches. Past it, continuing requires +one conscious act (**[Critical]**, a Request changes review, or /retry), +which is precisely what separates intent from automation. +``` + + + +### 75. run — Growth audit: a budget breach engages Critical-only AND makes the round a… + +In `run`. + +```text +Growth audit: a budget breach engages Critical-only AND makes the round a +growth-audit round. The agent audits the PR's approach on two axes — KISS +(name a structurally simpler alternative or prove each piece load-bearing) +and minimal change (every changed hunk traces to the PR's problem, an +accepted finding, or a failing check) — and records a machine-readable +verdict (sound/drift/conflict) in growth-audit.json, which the +verification gate requires in audit rounds. sound re-arms the window at +the current size (audit-gated /retry) and the loop continues; drift +simplifies first, then continues; conflict is the ONLY growth path to a +human, and it idles subsequent scans until a trusted human responds. A +size signal triggers a JUDGMENT, never a stop: solving the problem is +primary, growth control secondary. See docs/design/autofix-growth-audit.md. +An auth/access model error (401/402/403, "no access"/"does not exist") +never self-heals - only a maintainer can fix the key - and every retry +costs an agent run AND a PR comment. Cap those attempts far below +MAX_ROUNDS so the actionable "check the model key" message lands in an +hour instead of a day. Transient (429/5xx) errors keep the full budget. +``` + + + +### 76. run — Failed-check annotation patterns that mean the INFRASTRUCTURE died, not the code… + +In `run`. + +```text +Failed-check annotation patterns that mean the INFRASTRUCTURE died, not the +code — a self-hosted runner losing the server, the disk filling, a runner +shutdown, or a git fetch/clone dying mid-transfer. Such a check is red for a +reason unrelated to the PR and clears on a re-run (observed: #7490's E2E +"runner lost communication"; #6506's checkout "RPC failed; curl 92" / +"fetch-pack: invalid index-pack output" — both green on the rerun). The scan +auto-reruns those failed jobs ONCE, guarded by run_attempt so a persistent +infra problem cannot loop. Deliberately conservative — only unambiguous +machine/transport failures, never a bare test-level timeout, which could be +a real regression (a co-present timeout does not block a match — one +matching line classifies the run). Case-insensitive, vs the annotations. +``` + + + +### 77. run — Upper bound on review targets emitted per scan (fan-out defense-in-depth; excess… + +In `run`. + +```text +Upper bound on review targets emitted per scan (fan-out defense-in-depth; +excess is logged and deferred to the next scan). +TUNABLE WITHOUT A CODE CHANGE: set the repository variable to re-size the +loop as the takeover pool grows — Settings → Variables, no PR, no deploy. +The literal here is only the fallback when the variable is unset. +Why 30: 37 PRs carried autofix/takeover on 2026-08-08, so the previous +budget of 10 emitted at most 27% of the eligible set per tick and pushed +the rest a scan further out every time. It must also stay strictly above +the address matrix's max-parallel or the matrix can never fill, and the +same-repo candidate pool was 51, so 30 still bounds a pathological +backlog. RAISE BOTH TOGETHER: this must stay above QWEN_AUTOFIX_MAX_PARALLEL. +``` + + + +### 78. run — Upper bound on candidates INSPECTED per scan: idle candidates consume serial API… + +In `run`. + +```text +Upper bound on candidates INSPECTED per scan: idle candidates consume +serial API calls even when they emit nothing, and takeover widens the +candidate pool. Candidates are inspected NEWEST-first; past the budget +the oldest tail defers — old quiet PRs are the least likely to hold new +feedback, and a deferred PR with a live conflict is still picked up by +the shepherd's conflict lever. +TUNABLE WITHOUT A CODE CHANGE, for the same reason as the two above: this +one gates whether a PR is even LOOKED AT, so it has to grow ahead of the +candidate pool or the oldest PRs starve. The pool was 51 same-repo open +PRs on 2026-08-08, still under the 60 fallback. +``` + + + +### 79. run — Commit-status context stamped PENDING on a PR head when a scan dispatches a… + +In `run`. + +```text +Commit-status context stamped PENDING on a PR head when a scan dispatches +a review-address leg for it, and re-stamped SUCCESS by the leg on +checkout. It closes the visibility window between dispatch and leg +materialization: build-cli runs in between, and until the matrix expands +the leg does not exist in the live-run jobs view, so an overlapping scan +would re-dispatch the same PR. The scan treats a PENDING marker fresher +than DISPATCH_STATUS_TTL_MINUTES as busy (a run that dies before the leg +materializes leaves a marker that expires by age). Commit statuses only — +the check-run creation API needs a GitHub App, and this workflow +authenticates with a PAT. Same-repo heads only get a stamp: a fork head +sha does not exist in this repo's object store. +``` + + + +### 80. run — Consecutive-failure sub-cap, distinct from the total round cap (TAKEOVER_MAX_ROUNDS,… + +In `run`. + +```text +Consecutive-failure sub-cap, distinct from the total round cap (TAKEOVER_MAX_ROUNDS, documented at its declaration in qwen-autofix.yml). The +total cap bounds how many PRODUCTIVE rounds a PR may take; this bounds how +many rounds may fail IN A ROW with nothing pushed. Under takeover a PR gets +up to 100 rounds, but a PR that fails to push this many times running is not +iterating, it is stuck — a too-large / fast-conflicting PR whose fix keeps +timing out or failing the gate. Retrying at the same budget will not fix +that; a human has to rebase or split it. Any pushed round OR a legitimate +"no changes needed" no-op resets the streak, so this only ever fires on an +unbroken run of failures. Observed on #6723: 7 straight failed rounds (3 +timeouts, 4 gate rejections) over 8 hours, heading for 100. +``` + + + +### 81. run — Cumulative agent-timeout sub-cap, the sibling of the consecutive cap for the… + +In `run`. + +```text +Cumulative agent-timeout sub-cap, the sibling of the consecutive cap for +the failure shape it cannot see: timeouts INTERLEAVED with successful +rounds. A success resets the consecutive streak, but it does not make the +next timeout any cheaper — each one burns a full agent budget (~50m of +runner time) and pushes nothing. Observed on #7929: three timeouts with +pushed rounds in between, so the consecutive cap never fired and the PR +kept walking into the same wall; #7846 the same, twice. Counted over the +current counting window (window-scoped like every other census), so a +re-arm clears it along with the round counter. Counts BUDGET timeouts +only: silent-sandbox (idle) timeouts are infra, not PR size, and are +excluded — see qwen-autofix.md#af-073. +``` + + + +### 82. route · Decide phases — Real-time review triggers: process the SAME managed set the scheduled scan does,… + +In `route` · `Decide phases`. + +```text +Real-time review triggers: process the SAME managed set the +scheduled scan does, so feedback is picked up seconds after the +review instead of waiting for a schedule GitHub throttles hard +(the */10 cron actually lands every 40-70min on this repo). +Reviews must come from trusted senders (collaborators or the +review bot) so arbitrary commenters cannot force expensive +review-scan runs. Only pull_request_review:submitted triggers +(not per-comment events) to avoid redundant runs on +multi-comment reviews. +``` + + + +### 83. route · Decide phases — Comment-command sugar over the labels: TAKEOVER_COMMAND applies TAKEOVER_LABEL,… + +In `route` · `Decide phases`. + +```text +Comment-command sugar over the labels: TAKEOVER_COMMAND +applies TAKEOVER_LABEL, 'TAKEOVER_COMMAND stop' removes it — +nothing else. The label stays the single source of truth: +engagement and release happen ONLY via the label events +below; the command also posts acks directly in both +directions (#7999, #8002). Exact match on the trimmed body (constants, never +user-input parsing); allowed senders: the PR author (who may +lack label access) or a write+ collaborator. This immediately +narrows a previously fully-closed surface reopened under +maintainer mandate. +``` + + + +### 84. route · Decide phases — The bot only applies this label from takeover-command, which posts the engage… + +In `route` · `Decide phases`. + +```text +The bot only applies this label from takeover-command, +which posts the engage ack ITSELF: the labeled event +has been observed to simply not fire (#7999 — the +author read the silence as failure and removed the +label; #8002), so the user-visible ack must not +depend on this round-trip. Suppress only the ack — +the immediate scan is this event's real work and +still routes. +``` + + + +### 85. issue-autofix · Sanitize workspace git config — Rather than denylist each exec-vector family (which kept missing new ones), KEEP… + +Duplicated verbatim in 3 places: `issue-autofix` · `Sanitize workspace git config`, `build-cli` · `Sanitize workspace git config`, `review-address` · `Sanitize workspace git config`. + +```text +Rather than denylist each exec-vector family (which kept missing +new ones), KEEP a known-safe allowlist and --unset-all everything +else: this closes the whole class, including knobs not yet +enumerated. The kept set is only plumbing that carries no command +— repo format, remote, branch, fetch/gc/pack/index, safe.directory, +extensions, and submodule url/active/branch (NOT +submodule.*.update, which can be `!cmd`). actions/checkout +re-establishes remote/auth afterward. `|| true` on the grep: no +non-allowlisted keys (the steady state on an already-sanitized +runner) means grep exits 1, which would kill the step exactly +when there is nothing to clean. +``` + + + +### 86. issue-autofix · Stage trusted schema gate — The staged copy's trusted-base provenance holds at cp time only: RUNNER_TEMP is… + +In `issue-autofix` · `Stage trusted schema gate`. + +```text +The staged copy's trusted-base provenance holds at cp time only: +RUNNER_TEMP is writable by the branch/agent code later steps run +on this host, so record the digest in GITHUB_OUTPUT — expression +context, which a disk write after staging cannot reach — for the +PAT-bearing step to verify at invocation time. The trusted PATH is +recorded the same way and before any branch code runs, so a +$GITHUB_ENV-planted PATH/preload cannot swap the sha256sum/bash/git +the PAT step resolves (that would defeat the digest gate itself). +``` + + + +### 87. issue-autofix · Verification gate — Settings-schema freshness gate, shared with the triage-and-address verify step… + +In `issue-autofix` · `Verification gate`. + +```text +Settings-schema freshness gate, shared with the triage-and-address +verify step so the two copies cannot drift (rationale + the +generator crash guard live in the script). On failure it writes +outcome=failed to GITHUB_OUTPUT and exits 1. +Run the copy staged from the trusted base checkout: a PR branch +that predates the script does not contain it (bash would exit 127 +and kill the gate with no outcome), and the gate logic must come +from the trusted base, not the branch under verification. +``` + + + +### 88. issue-autofix · Withdraw claim on failure — Same hygiene as the PR-lane DETAIL_FILE excerpt: -c drops a partial multi-byte… + +In `issue-autofix` · `Withdraw claim on failure`. + +```text +Same hygiene as the PR-lane DETAIL_FILE excerpt: -c drops a +partial multi-byte sequence the byte-level head -c may have +split, and the markup neutralization stops a failure.md quoting +HTML whose opener sits before the 1500-byte cut and closer +after it (`' comment ('none' before any +takeover), every marker records the key of the window it was +produced in (win=…, legacy markers count as 'none'), and only +markers of the CURRENT window count toward the cap. Timestamp +windowing would race an in-flight address job selected before a +re-arm: its marker lands AFTER the ack and would instantly +re-cap the fresh window — key equality cannot. Within a window +the highest round wins (a terminal handoff marker must make the +scan skip regardless of order). +``` + + + +### 105. review-scan · Scan for PRs with new feedback — Seed for THIS window, from the ' from N' marker carried by the comment that… + +In `review-scan` · `Scan for PRs with new feedback`. + +```text +Seed for THIS window, from the ' from N' marker carried by +the comment that IS the window key — so it is window-scoped for +free, exactly like the key itself: a later /retry or a bare +/takeover opens a window whose anchor has no marker and the seed +returns to 0. Read by created_at equality against REARM_KEY, so a +seed from a SUPERSEDED window can never leak into the live one. +`scan` (not `capture`, which errors when absent) and `last` +(a hand-written marker further down a bot comment loses to the +workflow's own, which is always the final line). +``` + + + +### 106. review-scan · Scan for PRs with new feedback — Consent may have moved since PR_META: skip wins everywhere, and a takeover… + +In `review-scan` · `Scan for PRs with new feedback`. + +```text +Consent may have moved since PR_META: skip wins everywhere, +and a takeover notice additionally requires the label to +still be present — a label removed (or skip added) moments +ago must not receive a stale 'paused' notice. The read +FAILS CLOSED (mirrors takeover-ack): an unreadable label +state must not get a notice or the escalation label — +collapsing the failure to '' would ignore a concurrently +added skip for standard bot PRs. +``` + + + +### 107. review-scan · Scan for PRs with new feedback — The escalation label rides EVERY cap detection, noticed or not: the… + +In `review-scan` · `Scan for PRs with new feedback`. + +```text +The escalation label rides EVERY cap detection, noticed +or not: the once-per-window dedup suppresses repeat +comments, but the label is what makes a paused PR +filterable (the shepherd's auto-release ages from the +cap notice itself, not from the label) — and applying +it unconditionally backfills the already-paused fleet +via the scan rotation after this ships (idle backoff: +expect hours, not the first scan). +``` + + + +### 108. review-scan · Scan for PRs with new feedback — Conflict-park gate for the loop's OWN head move: while a conflict handoff pends… + +In `review-scan` · `Scan for PRs with new feedback`. + +```text +Conflict-park gate for the loop's OWN head move: while a +conflict handoff pends in the live window, an update-branch +merge re-fires every synchronize-triggered workflow on the new +head, and those loop-generated checks complete after both +park clocks — lifting the park with zero human activity, and +every woken round feeds CONSEC_FAIL toward a terminal lockout +on the exact PR a human is settling. Mirrors prepare's +conflict-handoff idempotence block (same marker scan, same +wake legs, same fail-closed fallbacks); a base that goes +stale during a park is re-handled by the address gate's own +stale-base retry once a human wakes a round. +``` + + + +### 109. review-scan · Scan for PRs with new feedback — Auto-update a PR that is red ONLY because of a stale base (see the… + +In `review-scan` · `Scan for PRs with new feedback`. + +```text +Auto-update a PR that is red ONLY because of a stale base (see the +MAIN_GREEN_CHECKS rationale above). The gate: the failing check also +passed for the PR that produced current main (a necessary-but-NOT- +sufficient signal — NOT proof main is healthy), and the PR is behind +or diverged, so it actually carries a stale base. Runs after the +round cap and pending-checks gates but before the feedback logic, +because a stuck-on-stale-base PR often has no NEW feedback at all (it +just sits red), which is exactly #7490's case. +``` + + + +### 110. review-scan · Scan for PRs with new feedback — STALE_BASE_REDS is pure jq over data already in memory (CHECKS_JSON,… + +In `review-scan` · `Scan for PRs with new feedback`. + +```text +STALE_BASE_REDS is pure jq over data already in memory +(CHECKS_JSON, MAIN_GREEN_CHECKS) — free, and far more selective +than the compare round-trip. Compute it FIRST and skip the network +call entirely when there is no stale-base red to act on (the common +case: a green PR, or one whose red check is also red on main). +CANCELLED is deliberately omitted from the PR-side selector: a +cancelled check is not evidence of a stale base. External commit +statuses are also excluded: a StatusContext exposes .context, not +.name/.workflowName, so it yields "" and select(. != "") drops it +(conservative — only Actions check-runs are matched). +``` + + + +### 111. review-address · Stage trusted schema gate and agent runner — The staged copies' trusted-base provenance holds at cp time only: RUNNER_TEMP is… + +In `review-address` · `Stage trusted schema gate and agent runner`. + +```text +The staged copies' trusted-base provenance holds at cp time only: +RUNNER_TEMP is writable by the branch/agent code later steps run +on this host, so the two digested copies — resanitize-git-config.sh +and run-autofix-review-verification.sh — record each digest in +GITHUB_OUTPUT — expression context, which a disk write after staging +cannot reach — for the invoking step to verify before execution. +(The step's other staged scripts carry no digest.) The gate runner is +pinned too: it runs the branch's own build/test between the two +gate passes, so an unverified copy would let the branch define +its own verdict. The trusted PATH is recorded before any branch +code runs, so a $GITHUB_ENV-planted PATH/preload cannot swap the +sha256sum/bash/git the steps resolve (that would defeat the digest +gate itself). +``` + + + +### 112. review-address · Prepare branch and feedback — This PAT-bearing step runs git (status/restore/fetch/checkout and a push… + +In `review-address` · `Prepare branch and feedback`. + +```text +This PAT-bearing step runs git (status/restore/fetch/checkout and a +push preflight) on the shared host BEFORE the agent/gate, so it +takes the same hermetic preamble the push steps do — the contract +test pins the executable lines equal across all three. Pin PATH and +drop the preload channels, strip git's env knobs, redirect the file +scopes to an unpredictable per-run throwaway (a concurrent job's +~/.gitconfig rewrite during this step's long window — staging, node +setup, npm ci, artifact download all sit before it — cannot steer +its git, and a fsmonitor/askpass/gpg.program plant cannot fire). +``` + + + +### 113. review-address · Prepare branch and feedback — ---- address-time eligibility recheck --------------------------- Fan-out can… + +In `review-address` · `Prepare branch and feedback`. + +```text +---- address-time eligibility recheck --------------------------- +Fan-out can hold this job queued for hours behind max-parallel, +and the matrix snapshot cannot see lifecycle changes: a PR +closed or merged while queued must not get a secret-bearing +agent run, a branch push, or a comment; an author/base/head +change must not be processed against stale assumptions. Re-fetch +and require the same shape the scan selected. A failed fetch is +UNKNOWN and discards too (fail closed — the next scan re-emits a +still-valid target). +``` + + + +### 114. review-address · Prepare branch and feedback — Maintainer-fork target: the branch does not exist on origin — fetch it (data… + +In `review-address` · `Prepare branch and feedback`. + +```text +Maintainer-fork target: the branch does not exist on origin — +fetch it (data only; hooks are severed) from the fork. A public +repo's fork heads are always public, so this fetch is anonymous: +`-c credential.helper=` resets the inherited helper list (a +planted global extraheader could 401 and hand a planted helper +this step's PAT — the same class the push sites reset against) +and `http.sslVerify=true` pins the transport. Fail closed on a +401 rather than authenticate. +``` + + + +### 115. review-address · Prepare branch and feedback — Allow-edits pushes ride the classic-PAT grant — GITHUB_TOKEN and fine-grained… + +In `review-address` · `Prepare branch and feedback`. + +```text +Allow-edits pushes ride the classic-PAT grant — GITHUB_TOKEN +and fine-grained PATs are documented as NOT receiving it. +Prove push access NOW, before an agent round is spent, instead +of 403ing at the report step after the work is done. +One-shot host-scoped helper like the push steps: the leading +empty credential.helper resets the inherited helper list (a +planted helper must never answer first) and http.sslVerify +pins the transport — full rationale → af-015. +``` + + + +### 116. review-address · Prepare branch and feedback — Release the dispatch-pending marker the emitting scan stamped on this head: the… + +In `review-address` · `Prepare branch and feedback`. + +```text +Release the dispatch-pending marker the emitting scan stamped on +this head: the leg has materialized, so from here the live-run +busy enumeration sees it and a PENDING status must not keep +overlapping scans away. Best-effort — a miss only delays the next +scan's view, and the marker expires by age anyway. A head that +moved since the dispatch left the stamp on the old sha; the +current head's rollup no longer shows it, so this re-stamp lands +where the next scan actually looks. Same-repo heads only — a fork +head sha is absent from this repo's object store and was never +stamped; dry runs stamp nothing either. +``` + + + +### 117. review-address · Prepare branch and feedback — Mechanical churn must not burn the budget: one dependency bump rewrites hundreds… + +In `review-address` · `Prepare branch and feedback`. + +```text +Mechanical churn must not burn the budget: one dependency bump +rewrites hundreds of package-lock.json lines and one +`generate:settings-schema` run regenerates the committed schema — +skimmed, not reviewed, so they measure no review burden. Keep the +list tight and name generated artifacts EXACTLY; a broad glob +would silently exempt hand-written files from the budget. Applied +to BOTH measurements: a lockfile can live under a test directory +(integration-tests/package-lock.json), and excluding it from one +side only would corrupt the NET_SRC subtraction. +``` + + + +### 118. review-address · Prepare branch and feedback — The instant THIS round's net was measured. Stamped into the growth-now marker so… + +In `review-address` · `Prepare branch and feedback`. + +```text +The instant THIS round's net was measured. Stamped into the +growth-now marker so the census filters on measurement +time, not the marker comment's created_at — the report posts the +marker only after the agent's ~120-minute run, so a round in +flight when a concurrent base update lands would otherwise pass a +created_at filter while carrying pre-update sums (#9114 R2-6). +Emitted ONLY when a measurement happened: an unmeasured attempt +that still stamped would be explicit in the per-run collapse and +displace the same run's real measurement (#9192 R4-3). +``` + + + +### 119. review-address · Prepare branch and feedback — NOTE (#9114 R2-8/R6-3): re-anchoring on an EXTERNAL head move (an author push)… + +In `review-address` · `Prepare branch and feedback`. + +```text +NOTE (#9114 R2-8/R6-3): re-anchoring on an EXTERNAL head move (an +author push) is deliberately NOT done here. The obvious signal — +comparing the checked-out head against the bot's last judged head +(autofix-redcheck) — is wrong: that marker records the head the +agent was GIVEN, before its own push, so it differs after every +pushing round and would re-anchor on the bot's own fixes, zeroing +the census in exactly the push regime the handoff exists for. A +correct version needs both a bot-authored-move test and a +PERSISTED cut (a one-round cut is re-admitted the next round); +that is its own change, tracked in #9114. +``` + + + +### 120. review-address · Prepare branch and feedback — KNOWN RESIDUAL (#9114): this sibling read still filters on the comment's… + +In `review-address` · `Prepare branch and feedback`. + +```text +KNOWN RESIDUAL (#9114): this sibling read still filters on the +comment's created_at, not a prepare-time measured= like the +growth-now read below. Its values are measured in prepare too, so a +round whose agent run straddles a base update can anchor the window +on pre-update values. Narrow (the update must land inside the +anchor round's own agent run) and first-wins, so it cannot be +re-poisoned later in the window; stamping measured= into the +growth-base marker is tracked with the rest of #9114 rather than +widening this change. +``` + + + +### 121. review-address · Prepare branch and feedback — Growth audit: a budget breach engages Critical-only AND makes the round a… + +In `review-address` · `Prepare branch and feedback`. + +```text +Growth audit: a budget breach engages Critical-only AND makes the +round a growth-audit round — a size signal triggers a JUDGMENT, +never a stop. The agent audits the approach (KISS + minimal +change, burden of proof inverted) and records a machine-readable +verdict the verification gate requires: sound re-arms the window +at the current size and the loop continues, drift simplifies +first, conflict is the only growth path to a human. Count this +window's prior per-round over-budget rounds for the audit's +context (the trajectory clause in feedback.md uses the same +number). Read this window's prior per-round growth markers +(written by the report step): + +Deduped by run=GITHUB_RUN_ID (the per-workflow-run id) and ORDERED +by measured=: the report post's bounded retry re-posts one run's +marker, and a failed job's re-run keeps the same run_id, so a run +collapses to its LATEST measurement — and that collapse happens +BEFORE the over/window/cutoff filters, or a re-run that came back +under budget would still be represented by its stale over=true +attempt. Within the collapse an explicit measured= beats the +created_at fallback: a re-run attempt that crashed BEFORE prepare +— or whose measurement failed — posts an inert over=false marker +with no measured=, whose fallback (post-run) timestamp would +otherwise outdate and erase the same run's real prepare-time +measurement. Every distinct address run has a fresh run_id. +KNOWN RESIDUAL (#9114): during the one-time deploy transition a +run whose FIRST attempt posted a legacy (no measured=) over=true +marker and whose re-run crashes before prepare still collapses +fallback-vs-fallback on created_at — the later inert marker wins +and erases the count. Self-limiting: once deployed, every real +measurement carries measured= and beats any inert marker. +round=/eval-watermark are NOT a safe identity — a state-triggered +lane (a persistent merge conflict selects the PR every scan with no +new evaluable feedback) freezes both NEWEST and ROUND, so distinct +over-budget runs would share them and collapse, stalling the count. +Filtered on measured= (the prepare-time measurement instant, NOT +the comment's post-agent created_at) after GROWTH_NOW_CUTOFF, so a +round measured against a pre-base-update tree is dropped rather +than counted in this window's census. KNOWN RESIDUAL (#9114): the +tree is fixed at the branch fetch/checkout while the cutoff comes +from ic.json fetched afterwards, so a base update landing between +the fetch and the measured_at stamp admits a pre-update marker; +self-heals at the next re-arm/base update. measured= is OPTIONAL +in the scan: markers posted before it existed fall back to their +comment's created_at, so deploying this does not blank the census +of a window that is already in flight. +The CURRENT run's own markers are excluded (run != GITHUB_RUN_ID): +a re-run of a failed job keeps the same run id and its failed +attempt already posted a marker, so counting it would over-report +the round's own attempt as a PRIOR one. +``` + + + +### 122. review-address · Prepare branch and feedback — Conflict-handoff idempotence: a conflict verdict parks the PR at a genuinely… + +In `review-address` · `Prepare branch and feedback`. + +```text +Conflict-handoff idempotence: a conflict verdict parks the PR at +a genuinely human call. Until a trusted human responds, scans +must not launch agents or post comments — review-bot regeneration +alone (an update-branch merge re-reviews every new head) would +otherwise churn one identical handoff after another. Wake only on +feedback the loop cannot produce itself: trusted-human +reviews/comments, or a failing check from OUTSIDE the Qwen Autofix +workflow (a CI build/test the loop did not run). The Qwen Autofix +workflow's OWN check runs are excluded wholesale: under a park no +address round can legitimately run, so any review-address check +newer than the marker is necessarily the conflict round's own +failed check (posted after the handoff) — counting it would let +the loop's own output unpark the very round it came from, and the +resulting wasted failure rounds feed CONSEC_FAIL toward a terminal +lockout on the exact PR a human is trying to settle. A manual +job re-run reaches prepare and parks green (no failed check), and +/retry remains the sanctioned lift. A /retry re-arm moves +LIVE_REARM_KEY past the marker's win= and lifts the park on its +own. +Two more loop-generated events must not wake: a stale-base +auto-update is the loop's OWN head move — the red checks it +REACTS to completed before its marker (they are the condition it +handles, not human feedback), so the checks leg counts only +failures completing after BOTH the conflict marker and the +latest base update; and CANCELLED never wakes — an +update-branch push cancels in-flight runs on the old head (and a +close/reopen does the same), which the loop produces without any +human. +The exclusion set is wider than the loop's own workflow: the +loop's SIBLING machinery produces check events too — the review +workflow re-fires on every head the loop's own base-update merge +creates, the CI-failure patrol re-runs flaky failures on the +UNCHANGED head by cron, and the fork lanes carry the loop's own +checks for fork PRs. All of it completes after both clocks with +no human anywhere in the input, so all of it is excluded by +name; and while a handoff pends, the loop performs NO head +moves at all (the scan's stale-base auto-update and the +conflict round's own stale-base retry both skip parked PRs), +so any check newer than both clocks is human-caused. +``` + + + +### 123. review-address · Prepare branch and feedback — Growth audit (a size signal triggers a JUDGMENT, never a stop): the window is… + +In `review-address` · `Prepare branch and feedback`. + +```text +Growth audit (a size signal triggers a JUDGMENT, never a stop): +the window is over its growth budget, so before any other work +the round audits the approach — two axes, burden of proof +inverted — and records a machine-readable verdict the +verification gate requires. sound re-arms the window and the +loop continues, drift simplifies first, conflict is the only +growth path to a human. The section carries the numbers and the +window's audit trail so a re-audit after a prior verdict must +bring new evidence to repeat it. +``` + + + +### 124. review-address · Post autofix status comment — The agent below runs for up to 130 minutes and the verification gate adds more,… + +In `review-address` · `Post autofix status comment`. + +```text +The agent below runs for up to 130 minutes and the verification gate adds +more, but nothing reaches the PR thread until "Push and report" at the +very end: a maintainer who just engaged takeover sees silence and cannot +tell a working round from a stuck one. The agent's output already +streams live to the Actions log, so publish that link up front. +Upserted by marker so one status comment per PR is EDITED each round +(edits notify nobody) rather than stacking a new comment against a +100-round cap. Runs after prepare so a revalidated-away stale duplicate +never announces a round it will not run. Best-effort: a status post that +fails warns and continues — it must never cost the round. +``` + + + +### 125. review-address · Triage and address — The primary attempt's real budget: 120m, with a 10-minute margin under the… + +In `review-address` · `Triage and address`. + +```text +The primary attempt's real budget: 120m, with a 10-minute margin +under the 130-minute step backstop above. The margin covers the +internal kill path (SIGTERM, 10s grace, SIGKILL, marker write); +if the step cap fires first, `agent-timeout` is never written and +the report step misclassifies the round as a crash. +QWEN_AUTOFIX_TIMEOUT_MS can only LOWER the fallback without a code +change: the run block clamps it to the 7,200,000 ms ceiling +(BUDGET_CAP_MS, the fallback itself), so raising the budget still +requires editing this default, BUDGET_CAP_MS, and the step backstop, +while a misconfigured variable degrades to a warning, not a misreport. +``` + + + +### 126. review-address · Triage and address — Prepare severed hooks for its PAT-bearing git ops; THIS step holds no PAT, so… + +In `review-address` · `Triage and address`. + +```text +Prepare severed hooks for its PAT-bearing git ops; THIS step +holds no PAT, so the branch's own hooks may check the agent's +commits again. HONEST LIMIT: the model key (OPENAI_API_KEY) IS +forwarded into the docker sandbox by the CLI, and the agent's +job is to build/test the branch — so on a taken-over +human-authored PR, branch-controlled scripts can read that key. +This is an accepted, explicit consequence of takeover +(triage+-gated, in-repo branches only, whose authors are +write-capable collaborators); keep AUTOFIX_OPENAI_API_KEY a +low-privilege, quota-bounded, rotatable key. +``` + + + +### 127. review-address · Repair deterministic rejection — Which side is corrupt is NOT known here — jq -s fails if EITHER input is… + +In `review-address` · `Repair deterministic rejection`. + +```text +Which side is corrupt is NOT known here — jq -s fails if +EITHER input is unparseable, and today's topology cannot +even produce a pre-existing carry (WORKDIR is wiped at run +start and there is exactly one repair step), so this branch +is defensive. Say what is certain: the merge failed, the +earlier set is kept, this round's is preserved unmerged. +The only loss path in this feature without a raw dump: the +newer set is discarded here and the eval watermark means +nothing re-derives it, so print it before deleting. `::` is +neutralized because the content is agent-written and a raw +`::` at line start would be parsed as a workflow command. +``` + + + +### 128. review-address · Finalize verification — The verdict travels WITH the attempt whose outcome is selected: a repair pass… + +In `review-address` · `Finalize verification`. + +```text +The verdict travels WITH the attempt whose outcome is +selected: a repair pass legitimately re-audits (its feedback +rebuild keeps the audit section; the SKILL mandates +audit-first), and the verdict its gate validated is the one +the round's code was judged by — binding the first pass +unconditionally dropped it. The :- fallback mirrors COMMITTED: +a repair that validated nothing leaves the first pass's +validated verdict as the record. +``` + + + +### 129. review-address · Finalize verification — Conclusion gate: fixed/noop are the ONLY outcomes that release the PAT push. A… + +In `review-address` · `Finalize verification`. + +```text +Conclusion gate: fixed/noop are the ONLY outcomes that release +the PAT push. A silent gate death (the step killed mid-check) +concludes failure, yet its step-output file stays discoverable +under $RUNNER_TEMP and appendable — a forged outcome=fixed + +verified_head must not flow to the push condition. Accept +fixed/noop only from a pass whose step concluded success; +anything else reads as a crashed gate (empty outcome → the +report's retry path), never as a verdict, and the audit bit +riding the tainted outputs is discarded with it. +``` + + + +### 130. review-address · Finalize verification — handoff and the two brake-violation rejections are deliberate, PUBLISHED… + +In `review-address` · `Finalize verification`. + +```text +handoff and the two brake-violation rejections are +deliberate, PUBLISHED verdicts, not failures: the agent +stopped under instruction (the growth-brake BLOCKED stop) +and the 'Report dry-run / failure' step posts the honest +headline, the handoff note, and the eval marker for all +three. Failing the job here would leave a red +review-address check that completes AFTER the marker's +ts=NEWEST — the next scan's N_FAILED_CHECKS includes this +workflow's own review-address checks, so it would count +the round's own rejection as NEW feedback and re-dispatch +the very item the headline promises not to retry, turning +one deliberate stop into a self-feeding loop. +``` + + + +### 131. review-address · Push and report — Growth-audit trail (+ re-arm on sound): audit rounds record the verdict under… + +In `review-address` · `Push and report`. + +```text +Growth-audit trail (+ re-arm on sound): audit rounds record the +verdict under the key the baseline was READ under — same rule as +the growth markers, same dead-key hazard (a supersede-exempt +round can report under a stale WINDOW after a re-arm). The +verdict comes from AUDIT_VERDICT — the verdict the verification +GATE validated and surfaced as a step output — NOT a re-read of +growth-audit.json: the branch's own build/tests run as the runner +user and WORKDIR is a predictable path they can write, so the +file could change after the gate looked. Re-arming is allowed +for completed rounds only ($1 = allow): a sound verdict whose +round then FAILED must not re-anchor the window — the failure +path re-measures under the same window instead. +``` + + + +### 132. review-address · Push and report — The mirror of the resolve above: a finding the agent did NOT resolve keeps its… + +In `review-address` · `Push and report`. + +```text +The mirror of the resolve above: a finding the agent did NOT +resolve keeps its thread open, and this answers it IN that thread. +Without it the reason sits only in the round summary, so the +reviewer who opens the still-open thread sees silence and cannot +tell their finding was read. Same neutralisation as the summary +body — a reply is model output posted verbatim under the bot +identity, so it could otherwise smuggle a forged control marker. +Best-effort: a reply failure must never fail a good push. +``` + + + +### 133. review-address · Push and report — Idempotence gate: a crash-and-rerun of this round, a same-run repair that… + +In `review-address` · `Push and report`. + +```text +Idempotence gate: a crash-and-rerun of this round, a +same-run repair that regenerates the dispositions, or a +later round whose agent rewrites an unchanged declination +must not post the same bot reply twice on one thread +(observed 2026-08-16: an identical reply posted three +times, #9296). Skip when the thread already carries a +comment by the bot whose body EQUALS the neutralised body +about to be posted; a changed body — a new reason in a +later round — still posts. Best-effort like the rest: with +a stale or empty threads view this degrades to the old +post-always behavior. +``` + + + +### 134. review-address · Push and report — The tree the gate verified is what gets pushed: assert HEAD is the gate's… + +In `review-address` · `Push and report`. + +```text +The tree the gate verified is what gets pushed: assert HEAD is +the gate's verified_head before touching credentials. A repo +redirect (a planted .git/commondir/GIT_DIR — the first defused +by resanitize, the second by the env strip) would otherwise let +`git rev-parse HEAD` and the push read an attacker repo whose +HEAD differs; this compares against the value the gate recorded +in GITHUB_OUTPUT (unreachable from a disk write). Empty +verified_head only on a noop, which does not reach this push. +``` + + + +### 135. review-address · Push and report — Bounded retry on the report post: this one comment carries the round's ENTIRE… + +In `review-address` · `Push and report`. + +```text +Bounded retry on the report post: this one comment carries the +round's ENTIRE persisted state (autofix-eval watermark/round, +redcheck head, growth baseline). The push has already landed, so +a transient API failure here loses the marker while keeping the +growth — the retry scan would re-anchor the baseline at the +post-push size and re-evaluate feedback it already addressed. +Three attempts bound that to genuine outages; the final failure +keeps today's semantics (step fails, no marker, next scan +retries the round). +``` + + + +### 136. review-address · Push and report — Crossing trigger, not an equality test: failure rounds also advance the round… + +In `review-address` · `Push and report`. + +```text +Crossing trigger, not an equality test: failure rounds also +advance the round counter, so `push@9, crash@10, push@11` +would skip an exact %10 check forever — and a failure-heavy +PR is the very PR the digest exists for. Post on the first +PUSHED round once 10+ rounds have accumulated since the last +digest in THIS window (or since the window opened). The +window opens at the round SEED, not at zero: a '/takeover +from 60' counter starts at 60, so the no-digest-yet baseline +is the seed — otherwise the seed-inflated counter digests on +the window's first push with a 1-2 round census. +``` + + + +### 137. review-address · Report dry-run / failure — This step also posts a round report (timeout / gate-rejection / abort), so it… + +In `review-address` · `Report dry-run / failure`. + +```text +This step also posts a round report (timeout / gate-rejection / +abort), so it writes the per-round growth-now marker too — else an +over-budget round that never reaches 'Push and report' leaves a +history gap and the census under-reports. Empty outputs +(prepare never ran) fall through the :-0/:-false marker fallbacks +to an inert over=false entry — measured= then OMITS itself (an +EMPTY measured= value matches no scan and would silently drop the +marker these fallbacks exist to keep); the reader falls back to +the comment's created_at. +``` + + + +### 138. review-address · Report dry-run / failure — handoff rounds end with a SUCCESS job status (a deliberate verdict), so they… + +In `review-address` · `Report dry-run / failure`. + +```text +handoff rounds end with a SUCCESS job status (a deliberate +verdict), so they must trigger on the outcome itself — without +this clause nothing would post and the loop would go silent on +exactly the rounds that most need a visible human handoff. The +two brake-violation rejections are green, published verdicts +for the same reason (finalize passes them so their own check +cannot re-select the PR), so they key on the outcome the same +way. +``` + + + +### 139. review-address · Report dry-run / failure — Cause-aware wording, most specific first — a model error and a gate crash each… + +In `review-address` · `Report dry-run / failure`. + +```text +Cause-aware wording, most specific first — a model error and a +gate crash each name the operator fix, while a bare no-output +crash points at a human. (The API clause runs first as +defense-in-depth: today run-agent writes failure.md on the +API-death path and the gate converts that to an explicit +outcome=failed, so GATE_CRASHED is false — but if the gate +ever changes, a model blip must not be reported as a gate +problem.) No Run log here — the report block below appends +it (avoid a duplicate). +``` + + + +### 140. review-address · Report dry-run / failure — A deliberate stop, not a failed fix: the agent stopped under instruction and… + +In `review-address` · `Report dry-run / failure`. + +```text +A deliberate stop, not a failed fix: the agent stopped +under instruction and deferred the item to a human. No +stale-base retry (there is no fix to re-attempt) and the +headline says what actually happened — the old +"could not produce a passing fix" wording reported the +brake's decision as a failure and buried the handoff. +Wording guard: no "🤖 AutoFix stopped" prefix — the fleet +shepherd's REASON regex reads that as a TERMINAL stop +reason, and this stop is transient (the loop stays +engaged); the shepherd contract test pins the distinction. +``` + + + +### 141. review-address · Report dry-run / failure — A brake VIOLATION, not a failed fix: the agent stopped under instruction but… + +In `review-address` · `Report dry-run / failure`. + +```text +A brake VIOLATION, not a failed fix: the agent stopped +under instruction but left a dirty workspace, so the +gate rejected the round under its own outcome (never +retryable — a repair pass would commit against the +brake). No stale-base probe: there is no fix to +re-attempt, and the probe's update-branch would merge +main for a retry that must not happen. The watermark +still advances (the feedback WAS read), so the item +hands to a human without any automatic-retry promise. +Wording guard: no "🤖 AutoFix stopped" prefix, same +reason as the handoff branch above. +``` + + + +### 142. review-address · Report dry-run / failure — The committed sibling of the dirty-handoff violation: the round HAS a commit… + +In `review-address` · `Report dry-run / failure`. + +```text +The committed sibling of the dirty-handoff violation: +the round HAS a commit beside handoff.md. The gate +rejected it non-retryably under its own outcome, so the +repair pass never engages and the handoff note survives +to be posted. The runner-side commit was NOT pushed — +only 'Push and report' (fixed/noop) publishes — so it is +discarded with the runner, the same committed_rc the +failure.md "commit discarded" wording keys on. +Wording guard: no "🤖 AutoFix stopped" prefix, same +reason as the handoff branch above. +``` + + + +### 143. review-address · Report dry-run / failure — A conflict round must PARK quietly at the human call: its own stale-base merge… + +In `review-address` · `Report dry-run / failure`. + +```text +A conflict round must PARK quietly at the human call: its +own stale-base merge would re-fire every synchronize- +triggered workflow on the new head, and those loop- +generated checks complete after the conflict marker this +same report posts — waking the very park it establishes. +The scan's stale-base auto-update carries the matching +gate; base staleness is re-handled by this retry once a +human wakes. +``` + + + +### 144. review-address · Report dry-run / failure — Prepare RAN (outcome success/failure) but produced no feedback to read — prepare… + +In `review-address` · `Report dry-run / failure`. + +```text +Prepare RAN (outcome success/failure) but produced no feedback to +read — prepare itself crashed or timed out before emitting a +verdict. Mark +terminal so the scan skips (it can't advance the watermark +without a read); do NOT imply MAX_ROUNDS attempts were made when +zero rounds happened. The headline states the real recovery +(delete the marker) rather than promise a re-trigger the +max-round guard would ignore. +``` + + + +### 145. review-address · Report dry-run / failure — CUMULATIVE timeout breaker — the sibling of the consecutive one above, for the… + +In `review-address` · `Report dry-run / failure`. + +```text +CUMULATIVE timeout breaker — the sibling of the consecutive +one above, for the failure shape it cannot see: timeouts +interleaved with pushed rounds. A push resets CONSEC_FAIL, +but it does not make the next timeout cheaper — each burns a +full agent budget with nothing to show (observed on #7929: +three timeouts with successes in between; #7846 twice). The +census reuses PRIOR_HEADS, so it is window-scoped exactly +like the consecutive one and a re-arm clears it. Only the +cap gate below overrides a would-be RETRY: a round already +terminal keeps its own headline (the consecutive breaker +included). The idle census and its warning run OUTSIDE that +guard: the all-idle shape terminates via the consecutive +breaker above, and that terminal run's job log is exactly +where the wedged runner must be named. +``` + + + +### 146. review-address · Report dry-run / failure — The agent committed (verify recorded committed=true before any gate could fail),… + +In `review-address` · `Report dry-run / failure`. + +```text +The agent committed (verify recorded committed=true before +any gate could fail), but every path that reaches this +handoff skipped "Push and report" — nothing landed on the +branch. Say so before the agent's address-summary.md, which +can read like a success and cite that now-discarded commit +SHA. Keyed on committed, NOT outcome=failed: the abort/no-op +paths (failure.md, dirty tree, unchanged branch, missing +summary) made no commit and keep the neutral framing below. +``` + + + +### 147. review-address · Report dry-run / failure — Same byte-budget hygiene as the English excerpt above. 3000 bytes ≈ 1000 CJK… + +In `review-address` · `Report dry-run / failure`. + +```text +Same byte-budget hygiene as the English excerpt above. 3000 +bytes ≈ 1000 CJK characters — roughly the information in the +1500-byte English excerpt. Beyond the `')" echo "🔄 re-armed PR #${PR}" # Management resumed — the escalation label is stale. 404 is the - # common case (the PR was never paused). Remove it only when the - # PR will actually be MANAGED after this re-arm (R5-2): the scan - # candidate population is bot-authored or takeover-labeled PRs - # only, so on an auto-released human PR (no takeover label, not - # bot-authored) /retry posts a marker nothing will act on — the - # label must stay as the only filterable escalation state. Skip - # also wins over re-arm everywhere (a frozen PR keeps its label), - # and the read FAILS CLOSED (mirrors takeover-ack's exit-1). + # common case (the PR was never paused). + # Full rationale → qwen-autofix.md#af-091 if ! RETRY_INFO="$(gh pr view "${PR}" --repo "${REPO}" --json labels,author 2> /dev/null)"; then echo "::warning::#${PR}: label state unreadable — keeping ${NEEDS_HUMAN_LABEL} (fail closed)" elif [[ "$(jq -r --arg t "${SKIP_LABEL}" '[.labels[]?.name] | index($t) != null' <<< "${RETRY_INFO}")" == "true" ]]; then @@ -2097,12 +1990,8 @@ jobs: # red run re-run later, or overlapping runs from quick label # toggles — must not DELETE a fresh cycle's needs-human and must # not post a marker that resets the round window (REARM_KEY is - # the newest engage marker). Label absent → the engagement ended - # after this event. A bot engage ack at/after the newest labeled - # event → the current cycle is already acked. Both skip without - # posting or touching labels; an unreadable history skips too - # (fail closed — the scan's NEED_ENGAGE_ACK dedup heals a - # genuinely missed ack, while nothing heals a stale marker). + # the newest engage marker). + # Full rationale → qwen-autofix.md#af-092 if [[ "${ACK}" == 'engaged' ]]; then if [[ "$(jq -r --arg t "${TAKEOVER_LABEL}" '[.labels[]?.name] | index($t) != null' <<< "${PR_STATE_INFO}")" != "true" ]]; then echo "⚠️ engaged ack skipped on #${PR}: ${TAKEOVER_LABEL} removed since the label event (stale ack)" @@ -2143,15 +2032,8 @@ jobs: BODY="$(printf '👋 Takeover released: the autofix loop will no longer engage this PR (an in-flight round, if any, completes its bounded work). Re-apply `%s` (or comment `%s`) to re-engage.\n\n
\n中文说明\n\n👋 已释放:autofix 循环不再介入此 PR(在飞的一轮如有,将完成其有界工作)。重新打上 `%s` 标签(或评论 `%s`)即可再次接管。\n\n
\n\n' "${TAKEOVER_LABEL}" "${TAKEOVER_COMMAND}" "${TAKEOVER_LABEL}" "${TAKEOVER_COMMAND}")" fi # The escalation label goes stale on a real engage or any release - # (a human is driving again). NOT on base-refused (nothing changed) - # and NOT on skip-blocked (management never resumed) — and a - # RELEASE onto a skip-frozen PR also keeps the label: nothing - # manages or restores that PR, so it must stay in the needs-human - # filter (R4-3). 404 is the common case (the PR was never paused). - # Runs BEFORE the ack comment: the state change already happened - # (the human toggled the label — the comment is purely - # informational), so a transient comment failure aborting this step - # under set -e must not strand the stale label. + # (a human is driving again). + # Full rationale → qwen-autofix.md#af-093 if [[ ( "${ACK}" == 'released' || "${ACK}" == 'engaged' ) && "${HAS_SKIP}" != 'true' ]]; then if ! NH_ERR="$(gh api -X DELETE "repos/${REPO}/issues/${PR}/labels/$(jq -rn --arg l "${NEEDS_HUMAN_LABEL}" '$l|@uri')" 2>&1)"; then [[ "${NH_ERR}" == *"HTTP 404"* ]] || echo "::warning::#${PR}: ${NEEDS_HUMAN_LABEL} removal failed — ${NH_ERR}" @@ -2199,15 +2081,8 @@ jobs: run: |- # Every lane that reaches this scan is supposed to hold the PAT: # route now declines the one event GitHub is known to run without - # secrets (a fork PR's own review) before it can set do_review. An - # empty PAT here is therefore a deleted or renamed secret, or a lane - # nobody has modelled yet — neither repaired by a later tick, and - # neither visible to a job `if:`, which cannot read the `secrets` - # context at all. - # It must not be quiet. With no credential every `gh` call below - # answers as if the repository held no PRs, so the scan would walk an - # empty candidate list and report a healthy fleet of zero — green, - # forever, while the whole loop is dead. + # secrets (a fork PR's own review) before it can set do_review. + # Full rationale → qwen-autofix.md#af-094 if [[ -z "${GITHUB_TOKEN}" ]]; then echo "::error::CI_DEV_BOT_PAT is empty in this ${EVENT_NAME} scan — every API call would be unauthenticated and the scan would report an empty fleet; check the repository secret" exit 1 @@ -2377,11 +2252,8 @@ jobs: # Candidate PRs: open, same-repo, targeting main, and either # authored by the dev-bot or opted in via TAKEOVER_LABEL. A PR # carrying SKIP_LABEL is excluded everywhere — skip wins over - # takeover when both are present. A forced PR must still pass all - # these checks. NOTE `.isCrossRepository == false` (fail-closed on a - # missing field), never a `// true` default piped through `not`: - # jq's // treats false as empty, so that form is false for EVERY - # input and silently green-no-op'd all forced dispatches. + # takeover when both are present. + # Full rationale → qwen-autofix.md#af-095 if [[ -n "${FORCED_PR}" ]]; then if ! META="$(read_forced_pr_meta)"; then echo "::error::Forced PR #${FORCED_PR} admission blocked: metadata_fetch_failed" @@ -2393,11 +2265,8 @@ jobs: # CLOSED on a missing isCrossRepository field (`.isCrossRepository # == false`, never a `// true | not` default — jq's // treats false # as empty, so that form is false for EVERY input and silently - # green-no-op'd all forced dispatches). Fork PRs are admitted under - # the scan's OWN fork rules (allow-edits on; the live write+ author - # gate runs in the shell case just below, mirroring the scan's - # per-candidate permission call) so the real-time route's fork - # pickup is not silently discarded here. + # green-no-op'd all forced dispatches). + # Full rationale → qwen-autofix.md#af-096 ADMISSION_REASON="$(forced_admission_reason <<< "${META}")" # Fork only: the author must hold write+ RIGHT NOW (the same # live-privilege rule the scan applies per candidate and @@ -2448,10 +2317,7 @@ jobs: # gate: that gate discards without writing a marker, so the # watermark never advances and an unfiltered scan would re-emit # the PR (checkout, npm ci, build) every tick forever. - # Rotating start offset (changes every ~10 minutes): a fixed - # newest-first order plus the inspection budget would starve the - # oldest tail FOREVER once the pool exceeds the budget; rotation - # guarantees every candidate is reached within pool/budget scans. + # Full rationale → qwen-autofix.md#af-097 ROT_OFF="$(( ($(date -u +%s) / 600) % 97 ))" CANDIDATES="$(jq -rs --arg skip "${SKIP_LABEL}" --argjson off "${ROT_OFF}" \ 'add @@ -2655,13 +2521,8 @@ jobs: # Dispatch-pending marker: a scan that dispatched this PR within # DISPATCH_STATUS_TTL_MINUTES may still be building its CLI # bundle — its matrix leg does not exist yet, so the live-run - # busy enumeration above cannot see it. The emitting scan stamped - # a PENDING status on the PR head at dispatch (same-repo heads - # only), and the leg re-stamps it SUCCESS on checkout; treat a - # fresh PENDING as busy. Costs no extra API call — the rollup is - # already in PR_META. Unlike the in-memory busy skip this runs - # after the metadata fetch, so it consumes inspection budget; - # acceptable because the case is rare (a PR dispatched <30m ago). + # busy enumeration above cannot see it. + # Full rationale → qwen-autofix.md#af-098 if jq -e --arg ctx "${DISPATCH_STATUS_CONTEXT}" --arg cut "${DISPATCH_CUTOFF}" ' [.statusCheckRollup[]? | select(.__typename == "StatusContext") | select(.context == $ctx) | select(.state == "PENDING") @@ -2704,12 +2565,8 @@ jobs: # Delay-window fallback: a review run parked BEFORE its job # starts (the 10-minute environment wait) has no review-pr # check-run yet, but a push now would still cancel it via - # synchronize. Only pull_request_target runs are cancelable — - # comment/review-triggered runs use per-run concurrency groups - # that a synchronize never cancels, so holding the round for - # one would defer autofix for nothing (R2-1). The scan fetched - # the newest run page once above; match by immutable head SHA or - # PR number, never by fork-controlled bare branch name. + # synchronize. + # Full rationale → qwen-autofix.md#af-099 REVIEW_RUN_STARTED_AT="$(jq -r --arg wf "${REVIEW_WF_ID}" --arg pr "${PR}" --arg head "${PR_HEAD_OID}" ' [ .workflow_runs[]? | select((.workflow_id | tostring) == $wf) @@ -2725,14 +2582,8 @@ jobs: fi # Auto-rerun a check that died on INFRASTRUCTURE, not the code (see - # INFRA_FAILURE_SIGNATURES). Only reached when the PR has a FAILED - # check; then, for each, we read its annotations and — if they carry - # a machine-death signature — rerun that run's failed jobs ONCE. The - # once is enforced by run_attempt: a run already retried to attempt 2 - # and still infra-failing is persistent, so we stop and leave it. No - # marker needed; the attempt counter is the guard, and after a rerun - # the attempt increments so the next scan skips it. Any API failure - # here is fail-safe: it just means no rerun. + # INFRA_FAILURE_SIGNATURES). + # Full rationale → qwen-autofix.md#af-100 if [[ -n "${PR_HEAD_OID}" && "${REVIEW_PR_LIVE}" != "true" ]] && jq -e 'any(.[]; ((.conclusion // .state // "") | IN("FAILURE","FAILED","ERROR","TIMED_OUT","ACTION_REQUIRED")) and (((.workflowName // "") != "Qwen Autofix") or ((.name // "") | startswith("review-address"))))' <<< "${CHECKS_JSON}" > /dev/null 2>&1; then RERAN_INFRA=false # Failed check-runs on this head, with their run id and annotation @@ -2775,11 +2626,7 @@ jobs: # startedAt is the only staleness clock: a check blocks only if it # started within the bound; one with no startedAt (queued, not yet # running) is not blocking (the next scan re-checks once it starts). - # The dispatch-pending marker is exempted by context: it is this - # loop's own StatusContext busy signal (no .workflowName/.name, so - # it passes the filters above) and its dedicated TTL check above is - # the authority on it — the 330-minute horizon here would keep a - # stranded marker blocking long past its TTL. + # Full rationale → qwen-autofix.md#af-101 HAS_PENDING_CHECKS="$(jq -r --arg cut "${PENDING_CUTOFF}" \ --argjson nonblocking "${NON_BLOCKING_CHECKS}" \ --arg ctx "${DISPATCH_STATUS_CONTEXT}" ' @@ -2802,13 +2649,8 @@ jobs: # Ack-on-defer (#8888): a real-time human review routed this # scan straight here, but the gate holds every mutation — from # the human's seat the bot read their review and then did - # nothing. Say so once per in-flight review (the marker embeds - # the review-pr check's startedAt, so a NEW review re-arms the - # ack). The feedback itself needs no ack: the watermark is not - # advanced on this skip, so the next scan after the review - # lands still sees and addresses it. Cron scans stay silent — - # nothing arrived in them that a human is waiting on, and the - # fleet table already shows the deferral. + # nothing. + # Full rationale → qwen-autofix.md#af-102 if [[ "${EVENT_NAME}" == 'pull_request_review' && "${DRY_RUN}" != "true" && "${REVIEW_SENDER}" != "${REVIEW_BOT}" ]]; then REVIEW_STARTED_AT="$(jq -r ' [ .[] @@ -2844,10 +2686,8 @@ jobs: # cannot predate the PR, and unlike the head commit date this never # advances when the branch is synced with main ("Update branch"/base # merge), so an early base-sync merge cannot bury a comment made before - # the first eval. If the metadata query failed (empty), fall back to an - # EMPTY floor — over-inclusive (evaluates all feedback once, then the - # first eval writes a marker) but never buries. NEVER fall back to the - # mutable head commit date: a base-sync HEAD would recreate the burial. + # the first eval. + # Full rationale → qwen-autofix.md#af-103 CREATED_WM="$(jq -r '.createdAt // ""' <<< "${PR_META}")" # PAGINATION NOTE: gh >= v2.31.0 merges all pages of a REST array @@ -2982,15 +2822,8 @@ jobs: | map(.ts) | map(select(. != "9999-12-31T23:59:59Z")) | max // ""' <<< "${MARKERS}")" # ROUND counting is windowed by KEY EQUALITY, not timestamps: the # current window key is the created_at of the latest - # '' comment ('none' before any - # takeover), every marker records the key of the window it was - # produced in (win=…, legacy markers count as 'none'), and only - # markers of the CURRENT window count toward the cap. Timestamp - # windowing would race an in-flight address job selected before a - # re-arm: its marker lands AFTER the ack and would instantly - # re-cap the fresh window — key equality cannot. Within a window - # the highest round wins (a terminal handoff marker must make the - # scan skip regardless of order). + # '' comment ('none' before any takeover). + # Full rationale → qwen-autofix.md#af-104 REARM_KEY="$(jq -r --arg ab "${AUTOFIX_BOT}" ' [ .[] | select((.user.login // "") == $ab) | select(((.body // "") | contains("")) @@ -3000,11 +2833,8 @@ jobs: # the comment that IS the window key — so it is window-scoped for # free, exactly like the key itself: a later /retry or a bare # /takeover opens a window whose anchor has no marker and the seed - # returns to 0. Read by created_at equality against REARM_KEY, so a - # seed from a SUPERSEDED window can never leak into the live one. - # `scan` (not `capture`, which errors when absent) and `last` - # (a hand-written marker further down a bot comment loses to the - # workflow's own, which is always the final line). + # returns to 0. + # Full rationale → qwen-autofix.md#af-105 ROUND_START="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${REARM_KEY}" ' [ .[] | select((.user.login // "") == $ab) | select((.created_at // "") == $key) @@ -3115,11 +2945,8 @@ jobs: # Consent may have moved since PR_META: skip wins everywhere, # and a takeover notice additionally requires the label to # still be present — a label removed (or skip added) moments - # ago must not receive a stale 'paused' notice. The read - # FAILS CLOSED (mirrors takeover-ack): an unreadable label - # state must not get a notice or the escalation label — - # collapsing the failure to '' would ignore a concurrently - # added skip for standard bot PRs. + # ago must not receive a stale 'paused' notice. + # Full rationale → qwen-autofix.md#af-106 LIVE_LABELS_JSON="$(gh pr view "${PR}" --repo "${REPO}" --json labels 2> /dev/null || echo '')" LIVE_LABELS="$(jq -r '[.labels[]?.name] | join(" ")' <<< "${LIVE_LABELS_JSON}" 2> /dev/null || echo '')" if [[ -z "${LIVE_LABELS_JSON}" ]]; then @@ -3153,10 +2980,8 @@ jobs: # or not: the once-per-window dedup suppresses repeat # comments, but the label is what makes a paused PR # filterable (the shepherd's auto-release ages from the - # cap notice itself, not from the label) — and applying - # it unconditionally backfills the already-paused fleet - # via the scan rotation after this ships (idle backoff: - # expect hours, not the first scan). + # cap notice itself). + # Full rationale → qwen-autofix.md#af-107 gh label create "${NEEDS_HUMAN_LABEL}" --repo "${REPO}" --color 'D93F0B' \ --description 'The autofix loop stopped on this PR — a human must re-arm, split, merge, or close it' \ 2> /dev/null || true @@ -3184,13 +3009,8 @@ jobs: # conflict handoff pends in the live window, an update-branch # merge re-fires every synchronize-triggered workflow on the new # head, and those loop-generated checks complete after both - # park clocks — lifting the park with zero human activity, and - # every woken round feeds CONSEC_FAIL toward a terminal lockout - # on the exact PR a human is settling. Mirrors prepare's - # conflict-handoff idempotence block (same marker scan, same - # wake legs, same fail-closed fallbacks); a base that goes - # stale during a park is re-handled by the address gate's own - # stale-base retry once a human wakes a round. + # park clocks — lifting the park with zero human activity. + # Full rationale → qwen-autofix.md#af-108 CONFLICT_PARKED='false' CONFLICT_SINCE_SCAN="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${REARM_KEY}" ' [ .[] | select((.user.login // "") == $ab) | . as $c | ($c.body // "") @@ -3239,24 +3059,13 @@ jobs: fleet_row "${PR}" 'conflict-parked' 'conflict handoff pending — no stale-base head move while parked' fi # Auto-update a PR that is red ONLY because of a stale base (see the - # MAIN_GREEN_CHECKS rationale above). The gate: the failing check also - # passed for the PR that produced current main (a necessary-but-NOT- - # sufficient signal — NOT proof main is healthy), and the PR is behind - # or diverged, so it actually carries a stale base. Runs after the - # round cap and pending-checks gates but before the feedback logic, - # because a stuck-on-stale-base PR often has no NEW feedback at all (it - # just sits red), which is exactly #7490's case. + # MAIN_GREEN_CHECKS rationale above). + # Full rationale → qwen-autofix.md#af-109 if [[ -n "${MAIN_HEAD}" && -n "${PR_HEAD_OID}" && "${CONFLICT_PARKED}" != 'true' ]]; then # STALE_BASE_REDS is pure jq over data already in memory # (CHECKS_JSON, MAIN_GREEN_CHECKS) — free, and far more selective - # than the compare round-trip. Compute it FIRST and skip the network - # call entirely when there is no stale-base red to act on (the common - # case: a green PR, or one whose red check is also red on main). - # CANCELLED is deliberately omitted from the PR-side selector: a - # cancelled check is not evidence of a stale base. External commit - # statuses are also excluded: a StatusContext exposes .context, not - # .name/.workflowName, so it yields "" and select(. != "") drops it - # (conservative — only Actions check-runs are matched). + # than the compare round-trip. + # Full rationale → qwen-autofix.md#af-110 STALE_BASE_REDS="$(jq -c -n \ --argjson checks "${CHECKS_JSON}" --argjson green "${MAIN_GREEN_CHECKS}" ' [ $checks[] @@ -3520,14 +3329,8 @@ jobs: # Rather than denylist each exec-vector family (which kept missing # new ones), KEEP a known-safe allowlist and --unset-all everything # else: this closes the whole class, including knobs not yet - # enumerated. The kept set is only plumbing that carries no command - # — repo format, remote, branch, fetch/gc/pack/index, safe.directory, - # extensions, and submodule url/active/branch (NOT - # submodule.*.update, which can be `!cmd`). actions/checkout - # re-establishes remote/auth afterward. `|| true` on the grep: no - # non-allowlisted keys (the steady state on an already-sanitized - # runner) means grep exits 1, which would kill the step exactly - # when there is nothing to clean. + # enumerated. + # Full rationale → qwen-autofix.md#af-085 git config --local --name-only --list 2>/dev/null \ | { grep -ivE '^(core\.(repositoryformatversion|bare|filemode|symlinks|ignorecase|precomposeunicode|logallrefupdates|worktree|hidedotfiles|protecthfs|protectntfs)|remote\..+\.(url|fetch|pushurl)|branch\.|extensions\.|gc\.|pack\.|fetch\.|index\.|safe\.|submodule\..+\.(url|active|branch))' || true; } \ | while IFS= read -r key; do git config --local --unset-all "$key" 2>/dev/null || true; done @@ -3744,14 +3547,8 @@ jobs: # Rather than denylist each exec-vector family (which kept missing # new ones), KEEP a known-safe allowlist and --unset-all everything # else: this closes the whole class, including knobs not yet - # enumerated. The kept set is only plumbing that carries no command - # — repo format, remote, branch, fetch/gc/pack/index, safe.directory, - # extensions, and submodule url/active/branch (NOT - # submodule.*.update, which can be `!cmd`). actions/checkout - # re-establishes remote/auth afterward. `|| true` on the grep: no - # non-allowlisted keys (the steady state on an already-sanitized - # runner) means grep exits 1, which would kill the step exactly - # when there is nothing to clean. + # enumerated. + # Full rationale → qwen-autofix.md#af-085 git config --local --name-only --list 2>/dev/null \ | { grep -ivE '^(core\.(repositoryformatversion|bare|filemode|symlinks|ignorecase|precomposeunicode|logallrefupdates|worktree|hidedotfiles|protecthfs|protectntfs)|remote\..+\.(url|fetch|pushurl)|branch\.|extensions\.|gc\.|pack\.|fetch\.|index\.|safe\.|submodule\..+\.(url|active|branch))' || true; } \ | while IFS= read -r key; do git config --local --unset-all "$key" 2>/dev/null || true; done @@ -3837,13 +3634,8 @@ jobs: # RUNNER_TEMP is writable by the branch/agent code later steps run # on this host, so record each digest in GITHUB_OUTPUT — expression # context, which a disk write after staging cannot reach — for the - # invoking step to verify before execution. The gate runner is - # pinned too: it runs the branch's own build/test between the two - # gate passes, so an unverified copy would let the branch define - # its own verdict. The trusted PATH is recorded before any branch - # code runs, so a $GITHUB_ENV-planted PATH/preload cannot swap the - # sha256sum/bash/git the steps resolve (that would defeat the digest - # gate itself). + # invoking step to verify before execution. + # Full rationale → qwen-autofix.md#af-111 echo "resanitize_sha256=$(sha256sum "${RUNNER_TEMP}/resanitize-git-config.sh" | cut -d' ' -f1)" >> "${GITHUB_OUTPUT}" echo "verify_runner_sha256=$(sha256sum "${RUNNER_TEMP}/run-autofix-review-verification.sh" | cut -d' ' -f1)" >> "${GITHUB_OUTPUT}" # The upsert script travels as CONTENT, not as a staged copy: it @@ -4054,12 +3846,8 @@ jobs: # This PAT-bearing step runs git (status/restore/fetch/checkout and a # push preflight) on the shared host BEFORE the agent/gate, so it # takes the same hermetic preamble the push steps do — the contract - # test pins the executable lines equal across all three. Pin PATH and - # drop the preload channels, strip git's env knobs, redirect the file - # scopes to an unpredictable per-run throwaway (a concurrent job's - # ~/.gitconfig rewrite during this step's long window — staging, node - # setup, npm ci, artifact download all sit before it — cannot steer - # its git, and a fsmonitor/askpass/gpg.program plant cannot fire). + # test pins the executable lines equal across all three. + # Full rationale → qwen-autofix.md#af-112 export PATH="${TRUSTED_PATH}" unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH \ GIT_CONFIG_PARAMETERS GIT_ALLOW_PROTOCOL GIT_PROXY_COMMAND \ @@ -4078,10 +3866,8 @@ jobs: # and the matrix snapshot cannot see lifecycle changes: a PR # closed or merged while queued must not get a secret-bearing # agent run, a branch push, or a comment; an author/base/head - # change must not be processed against stale assumptions. Re-fetch - # and require the same shape the scan selected. A failed fetch is - # UNKNOWN and discards too (fail closed — the next scan re-emits a - # still-valid target). + # change must not be processed against stale assumptions. + # Full rationale → qwen-autofix.md#af-113 PR_LIVE="$(gh pr view "${PR}" --repo "${REPO}" \ --json state,author,isCrossRepository,baseRefName,headRefName,labels,maintainerCanModify,headRepositoryOwner,headRepository,headRefOid 2> /dev/null || echo '{}')" LIVE_STATE="$(jq -r '.state // ""' <<< "${PR_LIVE}")" @@ -4162,13 +3948,8 @@ jobs: git config core.hooksPath /dev/null if [[ "${HEAD_REPO:-${REPO}}" != "${REPO}" ]]; then # Maintainer-fork target: the branch does not exist on origin — - # fetch it (data only; hooks are severed) from the fork. A public - # repo's fork heads are always public, so this fetch is anonymous: - # `-c credential.helper=` resets the inherited helper list (a - # planted global extraheader could 401 and hand a planted helper - # this step's PAT — the same class the push sites reset against) - # and `http.sslVerify=true` pins the transport. Fail closed on a - # 401 rather than authenticate. + # fetch it (data only; hooks are severed) from the fork. + # Full rationale → qwen-autofix.md#af-114 if ! git -c http.sslVerify=true -c credential.helper= fetch "https://github.com/${HEAD_REPO}.git" "refs/heads/${BRANCH}"; then echo "🫥 fork fetch failed for ${HEAD_REPO} (${BRANCH}) — discarding without action or marker" { @@ -4186,12 +3967,7 @@ jobs: git update-ref "refs/remotes/origin/${BRANCH}" "${BRANCH}" # Allow-edits pushes ride the classic-PAT grant — GITHUB_TOKEN # and fine-grained PATs are documented as NOT receiving it. - # Prove push access NOW, before an agent round is spent, instead - # of 403ing at the report step after the work is done. - # One-shot host-scoped helper like the push steps: the leading - # empty credential.helper resets the inherited helper list (a - # planted helper must never answer first) and http.sslVerify - # pins the transport — see 'Publish PR' for the full rationale. + # Full rationale → qwen-autofix.md#af-115 if ! git -c http.sslVerify=true -c credential.helper= -c credential."https://github.com".helper='!f(){ echo username=x-access-token; echo "password=${GITHUB_TOKEN}"; };f' \ push --no-verify --dry-run "https://github.com/${HEAD_REPO}.git" HEAD:"${BRANCH}" > /dev/null 2>&1; then echo "🫥 fork push preflight failed for ${HEAD_REPO} (allow-edits grant or PAT type) — discarding without action or marker" @@ -4216,13 +3992,8 @@ jobs: # Release the dispatch-pending marker the emitting scan stamped on # this head: the leg has materialized, so from here the live-run # busy enumeration sees it and a PENDING status must not keep - # overlapping scans away. Best-effort — a miss only delays the next - # scan's view, and the marker expires by age anyway. A head that - # moved since the dispatch left the stamp on the old sha; the - # current head's rollup no longer shows it, so this re-stamp lands - # where the next scan actually looks. Same-repo heads only — a fork - # head sha is absent from this repo's object store and was never - # stamped; dry runs stamp nothing either. + # overlapping scans away. + # Full rationale → qwen-autofix.md#af-116 if [[ "${DRY_RUN}" != "true" && "${HEAD_REPO:-${REPO}}" == "${REPO}" ]]; then gh api "repos/${REPO}/statuses/${CHECKED_OUT_HEAD}" -X POST \ -f state="success" -f context="${DISPATCH_STATUS_CONTEXT}" \ @@ -4415,12 +4186,8 @@ jobs: # Mechanical churn must not burn the budget: one dependency bump # rewrites hundreds of package-lock.json lines and one # `generate:settings-schema` run regenerates the committed schema — - # skimmed, not reviewed, so they measure no review burden. Keep the - # list tight and name generated artifacts EXACTLY; a broad glob - # would silently exempt hand-written files from the budget. Applied - # to BOTH measurements: a lockfile can live under a test directory - # (integration-tests/package-lock.json), and excluding it from one - # side only would corrupt the NET_SRC subtraction. + # skimmed, not reviewed, so they measure no review burden. + # Full rationale → qwen-autofix.md#af-117 GENERATED_EXCLUDES=(':(exclude,glob)**/package-lock.json' ':(exclude,glob)**/npm-shrinkwrap.json' ':(exclude)packages/vscode-ide-companion/schemas/settings.schema.json') # An orphan-history branch (fork takeover / adoption admits one — # nothing on this job's fetch requires a common ancestor) has no @@ -4448,36 +4215,19 @@ jobs: | select((.body // "") | contains("") ] | .[] @@ -4533,53 +4283,8 @@ jobs: fi # Growth audit: a budget breach engages Critical-only AND makes the # round a growth-audit round — a size signal triggers a JUDGMENT, - # never a stop. The agent audits the approach (KISS + minimal - # change, burden of proof inverted) and records a machine-readable - # verdict the verification gate requires: sound re-arms the window - # at the current size and the loop continues, drift simplifies - # first, conflict is the only growth path to a human. Count this - # window's prior per-round over-budget rounds for the audit's - # context (the trajectory clause in feedback.md uses the same - # number). Read this window's prior per-round growth markers - # (written by the report step): - # - # Deduped by run=GITHUB_RUN_ID (the per-workflow-run id) and ORDERED - # by measured=: the report post's bounded retry re-posts one run's - # marker, and a failed job's re-run keeps the same run_id, so a run - # collapses to its LATEST measurement — and that collapse happens - # BEFORE the over/window/cutoff filters, or a re-run that came back - # under budget would still be represented by its stale over=true - # attempt. Within the collapse an explicit measured= beats the - # created_at fallback: a re-run attempt that crashed BEFORE prepare - # — or whose measurement failed — posts an inert over=false marker - # with no measured=, whose fallback (post-run) timestamp would - # otherwise outdate and erase the same run's real prepare-time - # measurement. Every distinct address run has a fresh run_id. - # KNOWN RESIDUAL (#9114): during the one-time deploy transition a - # run whose FIRST attempt posted a legacy (no measured=) over=true - # marker and whose re-run crashes before prepare still collapses - # fallback-vs-fallback on created_at — the later inert marker wins - # and erases the count. Self-limiting: once deployed, every real - # measurement carries measured= and beats any inert marker. - # round=/eval-watermark are NOT a safe identity — a state-triggered - # lane (a persistent merge conflict selects the PR every scan with no - # new evaluable feedback) freezes both NEWEST and ROUND, so distinct - # over-budget runs would share them and collapse, stalling the count. - # Filtered on measured= (the prepare-time measurement instant, NOT - # the comment's post-agent created_at) after GROWTH_NOW_CUTOFF, so a - # round measured against a pre-base-update tree is dropped rather - # than counted in this window's census. KNOWN RESIDUAL (#9114): the - # tree is fixed at the branch fetch/checkout while the cutoff comes - # from ic.json fetched afterwards, so a base update landing between - # the fetch and the measured_at stamp admits a pre-update marker; - # self-heals at the next re-arm/base update. measured= is OPTIONAL - # in the scan: markers posted before it existed fall back to their - # comment's created_at, so deploying this does not blank the census - # of a window that is already in flight. - # The CURRENT run's own markers are excluded (run != GITHUB_RUN_ID): - # a re-run of a failed job keeps the same run id and its failed - # attempt already posted a marker, so counting it would over-report - # the round's own attempt as a PRIOR one. + # never a stop. + # Full rationale → qwen-autofix.md#af-121 OVER_ROUNDS_PRIOR=0 KISS_AUDIT='false' if [[ "${NET_MEASURED}" == 'true' ]]; then @@ -4607,44 +4312,8 @@ jobs: [[ "${KISS_AUDIT}" == 'true' ]] && echo "🔍 growth budget breached (source ${GROWTH_SRC} / test ${GROWTH_TEST} vs budgets ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES}; ${OVER_ROUNDS_PRIOR} prior over-budget round(s) this window) — this round is a growth-audit round." # Conflict-handoff idempotence: a conflict verdict parks the PR at - # a genuinely human call. Until a trusted human responds, scans - # must not launch agents or post comments — review-bot regeneration - # alone (an update-branch merge re-reviews every new head) would - # otherwise churn one identical handoff after another. Wake only on - # feedback the loop cannot produce itself: trusted-human - # reviews/comments, or a failing check from OUTSIDE the Qwen Autofix - # workflow (a CI build/test the loop did not run). The Qwen Autofix - # workflow's OWN check runs are excluded wholesale: under a park no - # address round can legitimately run, so any review-address check - # newer than the marker is necessarily the conflict round's own - # failed check (posted after the handoff) — counting it would let - # the loop's own output unpark the very round it came from, and the - # resulting wasted failure rounds feed CONSEC_FAIL toward a terminal - # lockout on the exact PR a human is trying to settle. A manual - # job re-run reaches prepare and parks green (no failed check), and - # /retry remains the sanctioned lift. A /retry re-arm moves - # LIVE_REARM_KEY past the marker's win= and lifts the park on its - # own. - # Two more loop-generated events must not wake: a stale-base - # auto-update is the loop's OWN head move — the red checks it - # REACTS to completed before its marker (they are the condition it - # handles, not human feedback), so the checks leg counts only - # failures completing after BOTH the conflict marker and the - # latest base update; and CANCELLED never wakes — an - # update-branch push cancels in-flight runs on the old head (and a - # close/reopen does the same), which the loop produces without any - # human. - # The exclusion set is wider than the loop's own workflow: the - # loop's SIBLING machinery produces check events too — the review - # workflow re-fires on every head the loop's own base-update merge - # creates, the CI-failure patrol re-runs flaky failures on the - # UNCHANGED head by cron, and the fork lanes carry the loop's own - # checks for fork PRs. All of it completes after both clocks with - # no human anywhere in the input, so all of it is excluded by - # name; and while a handoff pends, the loop performs NO head - # moves at all (the scan's stale-base auto-update and the - # conflict round's own stale-base retry both skip parked PRs), - # so any check newer than both clocks is human-caused. + # a genuinely human call. + # Full rationale → qwen-autofix.md#af-122 CONFLICT_SINCE="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${LIVE_REARM_KEY}" ' [ .[] | select((.user.login // "") == $ab) | . as $c | ($c.body // "") | [ scan("") ] | .[] @@ -4881,11 +4550,8 @@ jobs: # the window is over its growth budget, so before any other work # the round audits the approach — two axes, burden of proof # inverted — and records a machine-readable verdict the - # verification gate requires. sound re-arms the window and the - # loop continues, drift simplifies first, conflict is the only - # growth path to a human. The section carries the numbers and the - # window's audit trail so a re-audit after a prior verdict must - # bring new evidence to repeat it. + # verification gate requires. + # Full rationale → qwen-autofix.md#af-123 if [[ "${KISS_AUDIT}" == 'true' ]]; then echo "## Growth audit required — this window is over its growth budget" echo @@ -5038,13 +4704,8 @@ jobs: # The agent below runs for up to 130 minutes and the verification gate adds # more, but nothing reaches the PR thread until "Push and report" at the # very end: a maintainer who just engaged takeover sees silence and cannot - # tell a working round from a stuck one. The agent's output already - # streams live to the Actions log, so publish that link up front. - # Upserted by marker so one status comment per PR is EDITED each round - # (edits notify nobody) rather than stacking a new comment against a - # 100-round cap. Runs after prepare so a revalidated-away stale duplicate - # never announces a round it will not run. Best-effort: a status post that - # fails warns and continues — it must never cost the round. + # tell a working round from a stuck one. + # Full rationale → qwen-autofix.md#af-124 - name: 'Post autofix status comment' id: 'post_status' if: |- @@ -5108,15 +4769,8 @@ jobs: NO_PROXY: '127.0.0.1,localhost,::1' QWEN_HOME: '${{ runner.temp }}/qwen-autofix-review-home' # The primary attempt's real budget: 120m, with a 10-minute margin - # under the 130-minute step backstop above. The margin covers the - # internal kill path (SIGTERM, 10s grace, SIGKILL, marker write); - # if the step cap fires first, `agent-timeout` is never written and - # the report step misclassifies the round as a crash. - # QWEN_AUTOFIX_TIMEOUT_MS can only LOWER the fallback without a code - # change: the run block clamps it to the 7,200,000 ms ceiling - # (BUDGET_CAP_MS, the fallback itself), so raising the budget still - # requires editing this default, BUDGET_CAP_MS, and the step backstop, - # while a misconfigured variable degrades to a warning, not a misreport. + # under the 130-minute step backstop above. + # Full rationale → qwen-autofix.md#af-125 QWEN_TIMEOUT_MS: '${{ vars.QWEN_AUTOFIX_TIMEOUT_MS || 7200000 }}' CONFLICT: '${{ steps.prepare.outputs.conflict }}' BASE: 'main' @@ -5161,14 +4815,8 @@ jobs: rm -f "${WORKDIR}/failure.md" "${WORKDIR}/failure.zh.md" # Prepare severed hooks for its PAT-bearing git ops; THIS step # holds no PAT, so the branch's own hooks may check the agent's - # commits again. HONEST LIMIT: the model key (OPENAI_API_KEY) IS - # forwarded into the docker sandbox by the CLI, and the agent's - # job is to build/test the branch — so on a taken-over - # human-authored PR, branch-controlled scripts can read that key. - # This is an accepted, explicit consequence of takeover - # (triage+-gated, in-repo branches only, whose authors are - # write-capable collaborators); keep AUTOFIX_OPENAI_API_KEY a - # low-privilege, quota-bounded, rotatable key. + # commits again. + # Full rationale → qwen-autofix.md#af-126 git config core.hooksPath .husky # Clamp the override to the budget ceiling: a repo variable past # 7,200,000 ms (120m) would arm the timer past the 130-minute step @@ -5405,13 +5053,8 @@ jobs: # EITHER input is unparseable, and today's topology cannot # even produce a pre-existing carry (WORKDIR is wiped at run # start and there is exactly one repair step), so this branch - # is defensive. Say what is certain: the merge failed, the - # earlier set is kept, this round's is preserved unmerged. - # The only loss path in this feature without a raw dump: the - # newer set is discarded here and the eval watermark means - # nothing re-derives it, so print it before deleting. `::` is - # neutralized because the content is agent-written and a raw - # `::` at line start would be parsed as a workflow command. + # is defensive. + # Full rationale → qwen-autofix.md#af-127 _dfsize="$(wc -c < "${WORKDIR}/deferred-findings.json" 2> /dev/null | tr -d ' ')" if [[ -n "${_dfsize}" ]] && (( _dfsize > 4000 )); then echo "::warning::could not merge carried deferrals across the repair (one of the two sets is unparseable); keeping the carried set and preserving this round's as deferred-findings.unmerged.json. Raw content follows, TRUNCATED at 4000 of ${_dfsize} bytes — the full file rides this run's artifact dump:" @@ -5593,22 +5236,15 @@ jobs: # rebuild keeps the audit section; the SKILL mandates # audit-first), and the verdict its gate validated is the one # the round's code was judged by — binding the first pass - # unconditionally dropped it. The :- fallback mirrors COMMITTED: - # a repair that validated nothing leaves the first pass's - # validated verdict as the record. + # unconditionally dropped it. + # Full rationale → qwen-autofix.md#af-128 AUDIT_VERDICT="${REPAIR_AUDIT_VERDICT:-${FIRST_AUDIT_VERDICT}}" KISS_AUDIT="${REPAIR_KISS_AUDIT:-${FIRST_KISS_AUDIT}}" PASS_CONCLUSION="${REPAIR_CONCLUSION}" fi # Conclusion gate: fixed/noop are the ONLY outcomes that release - # the PAT push. A silent gate death (the step killed mid-check) - # concludes failure, yet its step-output file stays discoverable - # under $RUNNER_TEMP and appendable — a forged outcome=fixed + - # verified_head must not flow to the push condition. Accept - # fixed/noop only from a pass whose step concluded success; - # anything else reads as a crashed gate (empty outcome → the - # report's retry path), never as a verdict, and the audit bit - # riding the tainted outputs is discarded with it. + # the PAT push. + # Full rationale → qwen-autofix.md#af-129 if [[ "${OUTCOME}" == 'fixed' || "${OUTCOME}" == 'noop' ]] && [[ "${PASS_CONCLUSION}" != 'success' ]]; then echo "::error::verify pass claims outcome=${OUTCOME} but concluded '${PASS_CONCLUSION:-}' — discarding the claim (forged or crashed-gate outputs); NOT pushing" @@ -5637,13 +5273,8 @@ jobs: # stopped under instruction (the growth-brake BLOCKED stop) # and the 'Report dry-run / failure' step posts the honest # headline, the handoff note, and the eval marker for all - # three. Failing the job here would leave a red - # review-address check that completes AFTER the marker's - # ts=NEWEST — the next scan's N_FAILED_CHECKS includes this - # workflow's own review-address checks, so it would count - # the round's own rejection as NEW feedback and re-dispatch - # the very item the headline promises not to retry, turning - # one deliberate stop into a self-feeding loop. + # three. + # Full rationale → qwen-autofix.md#af-130 fixed|noop|handoff|dirty_handoff|committed_handoff) ;; *) exit 1 ;; esac @@ -5781,12 +5412,8 @@ jobs: # This step also posts a round report (timeout / gate-rejection / # abort), so it writes the per-round growth-now marker too — else an # over-budget round that never reaches 'Push and report' leaves a - # history gap and the census under-reports. Empty outputs - # (prepare never ran) fall through the :-0/:-false marker fallbacks - # to an inert over=false entry — measured= then OMITS itself (an - # EMPTY measured= value matches no scan and would silently drop the - # marker these fallbacks exist to keep); the reader falls back to - # the comment's created_at. + # history gap and the census under-reports. + # Full rationale → qwen-autofix.md#af-137 GROWTH_SRC: '${{ steps.prepare.outputs.growth_src }}' GROWTH_TEST: '${{ steps.prepare.outputs.growth_test }}' CRITICAL_ONLY_GROWTH: '${{ steps.prepare.outputs.critical_only_growth }}' @@ -5850,11 +5477,8 @@ jobs: # handoff rounds end with a SUCCESS job status (a deliberate # verdict), so they must trigger on the outcome itself — without # this clause nothing would post and the loop would go silent on - # exactly the rounds that most need a visible human handoff. The - # two brake-violation rejections are green, published verdicts - # for the same reason (finalize passes them so their own check - # cannot re-select the PR), so they key on the outcome the same - # way. + # exactly the rounds that most need a visible human handoff. + # Full rationale → qwen-autofix.md#af-138 if [[ "${OUTCOME:-unknown}" == "failed" || "${OUTCOME:-unknown}" == "handoff" || "${OUTCOME:-unknown}" == "dirty_handoff" || "${OUTCOME:-unknown}" == "committed_handoff" || "${JOB_STATUS:-}" != "success" ]]; then POST_HANDOFF=true fi @@ -5930,13 +5554,8 @@ jobs: MARK_TS='9999-12-31T23:59:59Z' # Cause-aware wording, most specific first — a model error and a # gate crash each name the operator fix, while a bare no-output - # crash points at a human. (The API clause runs first as - # defense-in-depth: today run-agent writes failure.md on the - # API-death path and the gate converts that to an explicit - # outcome=failed, so GATE_CRASHED is false — but if the gate - # ever changes, a model blip must not be reported as a gate - # problem.) No Run log here — the report block below appends - # it (avoid a duplicate). + # crash points at a human. + # Full rationale → qwen-autofix.md#af-139 if [[ -n "${API_ERROR_DETAIL}" ]]; then CAUSE="could not reach the model — ${API_ERROR_DETAIL}" CAUSE_ZH="无法连接模型 —— ${API_ERROR_DETAIL}" @@ -6006,15 +5625,8 @@ jobs: else if [[ "${OUTCOME:-}" == 'handoff' ]]; then # A deliberate stop, not a failed fix: the agent stopped - # under instruction and deferred the item to a human. No - # stale-base retry (there is no fix to re-attempt) and the - # headline says what actually happened — the old - # "could not produce a passing fix" wording reported the - # brake's decision as a failure and buried the handoff. - # Wording guard: no "🤖 AutoFix stopped" prefix — the fleet - # shepherd's REASON regex reads that as a TERMINAL stop - # reason, and this stop is transient (the loop stays - # engaged); the shepherd contract test pins the distinction. + # under instruction and deferred the item to a human. + # Full rationale → qwen-autofix.md#af-140 HEADLINE="🤖 AutoFix deferred this item to a human under instruction (round ${MARK_ROUND}/${MAX_ROUNDS}) — the agent's handoff note below names the decision and the options. The loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own." HEADLINE_ZH="🤖 AutoFix 已按指示将此项移交人工处理(第 ${MARK_ROUND}/${MAX_ROUNDS} 轮)—— 下方 agent 的 handoff 说明列出了待决决策与各选项。循环保持在线,仍会拾取新反馈与 base 冲突,但不会自行重试此项。" elif [[ "${OUTCOME:-}" == 'dirty_handoff' ]]; then @@ -6022,26 +5634,14 @@ jobs: # under instruction but left a dirty workspace, so the # gate rejected the round under its own outcome (never # retryable — a repair pass would commit against the - # brake). No stale-base probe: there is no fix to - # re-attempt, and the probe's update-branch would merge - # main for a retry that must not happen. The watermark - # still advances (the feedback WAS read), so the item - # hands to a human without any automatic-retry promise. - # Wording guard: no "🤖 AutoFix stopped" prefix, same - # reason as the handoff branch above. + # brake). + # Full rationale → qwen-autofix.md#af-141 HEADLINE="🤖 AutoFix rejected this round — the agent wrote a handoff but left a dirty workspace, violating the brake's commit-nothing stop (round ${MARK_ROUND}/${MAX_ROUNDS}). Nothing was committed; the uncommitted work was discarded with the runner. The handoff note below names what the agent stopped on, and the run log lists the dirty files. The loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own." HEADLINE_ZH="🤖 AutoFix 拒绝了本轮 —— agent 写了 handoff 却留下了脏工作区,违反 brake 的禁止提交停止(第 ${MARK_ROUND}/${MAX_ROUNDS} 轮)。未提交任何内容;未提交的改动已随 runner 丢弃。下方 handoff 说明指出了 agent 停止时的状态,运行日志列出了脏文件。循环保持在线,仍会拾取新反馈与 base 冲突,但不会自行重试此项。" elif [[ "${OUTCOME:-}" == 'committed_handoff' ]]; then # The committed sibling of the dirty-handoff violation: - # the round HAS a commit beside handoff.md. The gate - # rejected it non-retryably under its own outcome, so the - # repair pass never engages and the handoff note survives - # to be posted. The runner-side commit was NOT pushed — - # only 'Push and report' (fixed/noop) publishes — so it is - # discarded with the runner, the same committed_rc the - # failure.md "commit discarded" wording keys on. - # Wording guard: no "🤖 AutoFix stopped" prefix, same - # reason as the handoff branch above. + # the round HAS a commit beside handoff.md. + # Full rationale → qwen-autofix.md#af-142 HEADLINE="🤖 AutoFix rejected this round — the agent wrote a handoff but the round HAS a commit, violating the brake's commit-nothing stop (round ${MARK_ROUND}/${MAX_ROUNDS}). The commit was NOT pushed and is discarded with the runner; the run log lists it. The handoff note below names what the agent stopped on. The loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own." HEADLINE_ZH="🤖 AutoFix 拒绝了本轮 —— agent 写了 handoff 但本轮存在提交,违反 brake 的禁止提交停止(第 ${MARK_ROUND}/${MAX_ROUNDS} 轮)。该提交未推送,已随 runner 丢弃;运行日志中有记录。下方 handoff 说明指出了 agent 停止时的状态。循环保持在线,仍会拾取新反馈与 base 冲突,但不会自行重试此项。" else @@ -6054,9 +5654,7 @@ jobs: # triggered workflow on the new head, and those loop- # generated checks complete after the conflict marker this # same report posts — waking the very park it establishes. - # The scan's stale-base auto-update carries the matching - # gate; base staleness is re-handled by this retry once a - # human wakes. + # Full rationale → qwen-autofix.md#af-143 if [[ "${AUDIT_VERDICT:-}" != 'conflict' ]]; then MAIN_HEAD_R="$(gh api "repos/${REPO}/commits/${DEFAULT_BRANCH:-main}" --jq '.sha' 2> /dev/null || echo '')" if [[ -n "${MAIN_HEAD_R}" && -n "${REPORT_HEAD}" ]]; then @@ -6122,12 +5720,8 @@ jobs: else # Prepare RAN (outcome success/failure) but produced no feedback to # read — prepare itself crashed or timed out before emitting a - # verdict. Mark - # terminal so the scan skips (it can't advance the watermark - # without a read); do NOT imply MAX_ROUNDS attempts were made when - # zero rounds happened. The headline states the real recovery - # (delete the marker) rather than promise a re-trigger the - # max-round guard would ignore. + # verdict. + # Full rationale → qwen-autofix.md#af-144 MARK_ROUND="${MAX_ROUNDS}" HEADLINE="🤖 AutoFix could not start evaluation — it crashed or timed out before reading the feedback, so no fix was attempted. This PR is now marked terminal and future scans (including forced dispatch) will skip it. To recover: delete this bot's terminal \`autofix-eval\` marker comment, then re-trigger if the failure looked transient." HEADLINE_ZH="🤖 AutoFix 未能开始评估 —— 在读取反馈之前崩溃或超时,因此未尝试修复。本 PR 已被标记为终止,后续扫描(包括强制 dispatch)都会跳过它。恢复方法:删除本 bot 的终止性 \`autofix-eval\` 标记评论;若失败看起来是瞬时的,再重新触发。" @@ -6170,18 +5764,8 @@ jobs: fi # CUMULATIVE timeout breaker — the sibling of the consecutive # one above, for the failure shape it cannot see: timeouts - # interleaved with pushed rounds. A push resets CONSEC_FAIL, - # but it does not make the next timeout cheaper — each burns a - # full agent budget with nothing to show (observed on #7929: - # three timeouts with successes in between; #7846 twice). The - # census reuses PRIOR_HEADS, so it is window-scoped exactly - # like the consecutive one and a re-arm clears it. Only the - # cap gate below overrides a would-be RETRY: a round already - # terminal keeps its own headline (the consecutive breaker - # included). The idle census and its warning run OUTSIDE that - # guard: the all-idle shape terminates via the consecutive - # breaker above, and that terminal run's job log is exactly - # where the wedged runner must be named. + # interleaved with pushed rounds. + # Full rationale → qwen-autofix.md#af-145 # Idle (silent-sandbox) timeouts are EXCLUDED from this cap: # a wedged runner/docker is not this PR being too big, an idle # round dies at QWEN_IDLE_TIMEOUT_MS having produced no byte @@ -6246,11 +5830,8 @@ jobs: # The agent committed (verify recorded committed=true before # any gate could fail), but every path that reaches this # handoff skipped "Push and report" — nothing landed on the - # branch. Say so before the agent's address-summary.md, which - # can read like a success and cite that now-discarded commit - # SHA. Keyed on committed, NOT outcome=failed: the abort/no-op - # paths (failure.md, dirty tree, unchanged branch, missing - # summary) made no commit and keep the neutral framing below. + # branch. + # Full rationale → qwen-autofix.md#af-146 echo "⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:" else echo "**What I found before stopping:**" @@ -6309,12 +5890,8 @@ jobs: fi # Same byte-budget hygiene as the English excerpt above. 3000 # bytes ≈ 1000 CJK characters — roughly the information in the - # 1500-byte English excerpt. Beyond the `