From c229f75440ca67e49735b3da79defe9a95360b85 Mon Sep 17 00:00:00 2001 From: verify Date: Wed, 12 Aug 2026 13:11:47 +0800 Subject: [PATCH 1/8] feat(autofix): brake review-round diff growth with per-window src/test budgets Managed PRs bloat while still under the round threshold: #8853 grew from 315 to 1393 net lines in four bot rounds (86% of the growth was test lines; one 'harden per review feedback' round alone added 609), and #8276 grew ~2700 net lines under management. Every push regenerates review suggestions, and every window re-arm reopens the five suggestion-capable rounds, so the round-based Critical-only brake never binds on the size dimension. The prepare step now measures the branch's net diff vs the merge base, split into test lines (*.test.* / *.spec.* files, __snapshots__/, test-utils/, integration-tests/) and source lines, anchors a per-counting-window baseline marker (autofix-growth-base, first-wins, riding the window's first report comment like autofix-redcheck), and engages Critical-only mode early once either dimension outgrows its budget (vars.QWEN_AUTOFIX_GROWTH_BUDGET_{SRC,TEST}_LINES, default 400). Two budgets rather than one because the measured bloat concentrates in tests; a single budget cannot be tightened on tests without strangling source fixes. The deferral preamble names the actual cause, and /retry or re-engaging takeover re-anchors the baseline with the fresh window. Critical findings, Request changes reviews, in-budget maintainer feedback, failed checks, and conflict resolution flow exactly as before. --- .github/workflows/qwen-autofix.yml | 116 +++++++++++- .qwen/skills/autofix/SKILL.md | 7 +- scripts/tests/qwen-autofix-workflow.test.js | 186 +++++++++++++++++++- 3 files changed, 300 insertions(+), 9 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index 3c81ac64bc6..39ed6ac7785 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -137,6 +137,31 @@ env: # one conscious act (**[Critical]**, a Request changes review, or /retry), # which is precisely what separates intent from automation. CRITICAL_ONLY_HUMAN_BATCHES: '2' + # Net-diff growth budgets per counting window — the SIZE sibling of the + # round brake above. CRITICAL_ONLY_AFTER_ROUND counts rounds, but one round + # can add hundreds of lines (#8853 grew 315 → 1393 net lines in four bot + # rounds, +609 in a single "harden per review feedback" round; #8276 grew + # ~2700 net lines under management), so a managed PR can bloat drastically + # while still under the round threshold — and every window re-arm reopens + # the suggestion valve. The first round of a counting window records the + # PR's net size (insertions minus deletions vs the merge base) as that + # window's baseline; once live growth beyond the baseline exceeds a budget, + # Critical-only mode engages early. Everything Critical-only preserves + # still flows — Critical findings, Request changes reviews, in-budget + # maintainer feedback, failed checks, conflict resolution — only the + # suggestion channel stops. `@qwen-code /retry` (or re-engaging takeover) + # opens a fresh window and re-anchors the baseline at the current size. + # TWO budgets, not one: measured bloat concentrates in TESTS (#8853's + # growth was 86% test lines — every round pins ever-more-marginal behavior; + # #8276's was 78%), so a single budget is effectively spent by test growth + # and cannot be tightened on tests without also strangling source fixes. + # Test lines are *.test.*/*.spec.* files, __snapshots__/, test-utils/, and + # integration-tests/ (the pathspec lives in the prepare step); source is + # everything else. Either budget tripping engages the brake. + # TUNABLE WITHOUT A CODE CHANGE like the scan budgets above; a malformed + # value falls back to its default at the read site. + GROWTH_BUDGET_SRC_LINES: '${{ vars.QWEN_AUTOFIX_GROWTH_BUDGET_SRC_LINES || 400 }}' + GROWTH_BUDGET_TEST_LINES: '${{ vars.QWEN_AUTOFIX_GROWTH_BUDGET_TEST_LINES || 400 }}' # 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 @@ -4012,9 +4037,67 @@ jobs: STALE='true' echo "⛔ live round ${ROUND} already at MAX_ROUNDS (${MAX_ROUNDS}) — discarding without action or marker" fi + # Growth brake: measure the PR's net size (insertions minus + # deletions vs the merge base), split into test lines and source + # lines, and compare against the sizes recorded when this counting + # window opened. The baseline rides in the window's first pushed or + # no-op report comment as its OWN marker (the autofix-redcheck + # pattern — the positional autofix-eval parsers never change), so a + # /retry or takeover re-engage re-anchors it with the window. + # First-wins on read: a duplicate marker in one window cannot move + # an anchored baseline. A handoff round writes no baseline; nothing + # was pushed, so the next round re-measures the same size. + # Growth-triggered Critical-only reuses the round brake's entire + # deferral machinery below; the human batch budget stays + # round-scoped, so maintainer feedback flows exactly as today. + if [[ ! "${GROWTH_BUDGET_SRC_LINES}" =~ ^[0-9]{1,7}$ ]]; then + echo "::warning::GROWTH_BUDGET_SRC_LINES='${GROWTH_BUDGET_SRC_LINES}' is not a line count; using 400" + GROWTH_BUDGET_SRC_LINES=400 + fi + if [[ ! "${GROWTH_BUDGET_TEST_LINES}" =~ ^[0-9]{1,7}$ ]]; then + echo "::warning::GROWTH_BUDGET_TEST_LINES='${GROWTH_BUDGET_TEST_LINES}' is not a line count; using 400" + GROWTH_BUDGET_TEST_LINES=400 + fi + # Binary files report "-" in numstat; count them as 0 lines. + sum_numstat() { awk '{ if ($1 != "-") a += $1; if ($2 != "-") d += $2 } END { print a - d + 0 }'; } + TEST_PATHSPEC=(':(glob)**/*.test.*' ':(glob)**/*.spec.*' ':(glob)**/__snapshots__/**' ':(glob)**/test-utils/**' ':(glob)integration-tests/**') + NET_TOTAL="$(git diff --numstat origin/main...HEAD | sum_numstat)" + NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" | sum_numstat)" + NET_SRC=$(( NET_TOTAL - NET_TEST )) + GROWTH_BASELINE="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${LIVE_REARM_KEY}" ' + [ .[] | select((.user.login // "") == $ab) | . as $c | ($c.body // "") + | [ scan("") ] | .[] + | {src: (.[0] | tonumber), test: (.[1] | tonumber), win: .[2], at: ($c.created_at // "")} ] + | map(select(.win == $key)) | sort_by(.at) + | .[0] // empty | "\(.src) \(.test)"' "${WORKDIR}/ic.json")" + GROWTH_BASE_NEW='false' + if [[ "${GROWTH_BASELINE}" =~ ^(-?[0-9]+)\ (-?[0-9]+)$ ]]; then + BASE_SRC="${BASH_REMATCH[1]}" + BASE_TEST="${BASH_REMATCH[2]}" + else + BASE_SRC="${NET_SRC}" + BASE_TEST="${NET_TEST}" + GROWTH_BASE_NEW='true' + fi + GROWTH_SRC=$(( NET_SRC - BASE_SRC )) + GROWTH_TEST=$(( NET_TEST - BASE_TEST )) + { + echo "growth_base_new=${GROWTH_BASE_NEW}" + echo "growth_base_src=${BASE_SRC}" + echo "growth_base_test=${BASE_TEST}" + } >> "${GITHUB_OUTPUT}" + echo "📏 net diff src ${NET_SRC} / test ${NET_TEST} lines (window baseline ${BASE_SRC}/${BASE_TEST}, growth ${GROWTH_SRC}/${GROWTH_TEST}, budgets ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" + CRITICAL_ONLY='false' + CRITICAL_ONLY_ROUNDS='false' + CRITICAL_ONLY_GROWTH='false' if [[ "${ROUND}" -ge "${CRITICAL_ONLY_AFTER_ROUND}" ]]; then CRITICAL_ONLY='true' + CRITICAL_ONLY_ROUNDS='true' + fi + if [[ "${GROWTH_SRC}" -gt "${GROWTH_BUDGET_SRC_LINES}" || "${GROWTH_TEST}" -gt "${GROWTH_BUDGET_TEST_LINES}" ]]; then + CRITICAL_ONLY='true' + CRITICAL_ONLY_GROWTH='true' fi # Which trusted humans have exhausted their per-window regular # feedback budget (see CRITICAL_ONLY_HUMAN_BATCHES). A batch is @@ -4087,10 +4170,27 @@ jobs: rm -f "${WORKDIR}/deferred-feedback.md" if [[ "${CRITICAL_ONLY}" == "true" ]]; then PR_URL="https://github.com/${REPO}/pull/${PR}" + # Name the cause(s) precisely: a maintainer reading "after five + # rounds" on a round-2 PR that tripped the GROWTH budget would + # reasonably conclude the brake misfired. + GROWTH_CLAUSE_EN="the PR's diff grew src +${GROWTH_SRC} / test +${GROWTH_TEST} net lines beyond this counting window's baseline (budgets: ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" + GROWTH_CLAUSE_ZH="本计数窗口内 diff 净增长已达 源码 +${GROWTH_SRC} / 测试 +${GROWTH_TEST} 行(预算 ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" + ROUNDS_CLAUSE_EN="${CRITICAL_ONLY_AFTER_ROUND} change-producing rounds are complete" + ROUNDS_CLAUSE_ZH="已完成 ${CRITICAL_ONLY_AFTER_ROUND} 个产生改动的轮次" + if [[ "${CRITICAL_ONLY_ROUNDS}" == 'true' && "${CRITICAL_ONLY_GROWTH}" == 'true' ]]; then + CAUSE_EN="${ROUNDS_CLAUSE_EN} and ${GROWTH_CLAUSE_EN}" + CAUSE_ZH="${ROUNDS_CLAUSE_ZH},且${GROWTH_CLAUSE_ZH}" + elif [[ "${CRITICAL_ONLY_GROWTH}" == 'true' ]]; then + CAUSE_EN="${GROWTH_CLAUSE_EN}" + CAUSE_ZH="${GROWTH_CLAUSE_ZH}" + else + CAUSE_EN="${ROUNDS_CLAUSE_EN}" + CAUSE_ZH="${ROUNDS_CLAUSE_ZH}" + fi { echo '## Deferred non-Critical feedback' echo - echo "Critical-only mode is active after ${CRITICAL_ONLY_AFTER_ROUND} change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used ${CRITICAL_ONLY_HUMAN_BATCHES} regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (\`@qwen-code /retry\` starts a fresh counting window.)" + echo "Critical-only mode is active: ${CAUSE_EN}. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used ${CRITICAL_ONLY_HUMAN_BATCHES} regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (\`@qwen-code /retry\` starts a fresh counting window.)" echo jq -r --arg wm "${WATERMARK}" --arg rb "${REVIEW_BOT}" --arg ab "${AUTOFIX_BOT}" \ --arg pr_url "${PR_URL}" --argjson over "${OVER_BUDGET_AUTHORS}" ' @@ -4146,7 +4246,7 @@ jobs: echo '
' echo '中文说明' echo - echo "完成 ${CRITICAL_ONLY_AFTER_ROUND} 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 ${CRITICAL_ONLY_HUMAN_BATCHES} 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 \`@qwen-code /retry\` 可开启新的计数窗口。)" + echo "已进入仅处理 Critical 的模式:${CAUSE_ZH}。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 ${CRITICAL_ONLY_HUMAN_BATCHES} 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 \`@qwen-code /retry\` 可开启新的计数窗口。)" echo echo '
' } > "${WORKDIR}/deferred-feedback.md" @@ -4720,6 +4820,12 @@ jobs: MODEL: '${{ vars.QWEN_AUTOFIX_MODEL || vars.QWEN_PR_REVIEW_MODEL }}' CHECKED_OUT_HEAD: '${{ steps.prepare.outputs.checked_out_head }}' VERIFIED_HEAD: '${{ steps.final_verify.outputs.verified_head }}' + # Growth-brake baseline: written into this window's FIRST report + # comment only (growth_base_new), so later rounds' first-wins parse + # keeps the anchor. Empty when prepare exited early — no marker. + GROWTH_BASE_NEW: '${{ steps.prepare.outputs.growth_base_new }}' + GROWTH_BASE_SRC: '${{ steps.prepare.outputs.growth_base_src }}' + GROWTH_BASE_TEST: '${{ steps.prepare.outputs.growth_base_test }}' run: |- # The head the agent actually evaluated — captured in prepare before # any mutation, not the report-time remote head (which can move @@ -4987,6 +5093,9 @@ jobs: echo echo "" echo "" + if [[ "${GROWTH_BASE_NEW}" == 'true' ]]; then + echo "" + fi } > "${WORKDIR}/report.md" STATUS="pushed (round ${NEXT_ROUND}/${MAX_ROUNDS})" else @@ -5008,6 +5117,9 @@ jobs: echo echo "" echo "" + if [[ "${GROWTH_BASE_NEW}" == 'true' ]]; then + echo "" + fi } > "${WORKDIR}/report.md" STATUS="no action needed" fi diff --git a/.qwen/skills/autofix/SKILL.md b/.qwen/skills/autofix/SKILL.md index 833d7c10f65..32056bdddf5 100644 --- a/.qwen/skills/autofix/SKILL.md +++ b/.qwen/skills/autofix/SKILL.md @@ -304,8 +304,11 @@ implement — satisfying a nit is never a reason to bloat the code. worth the diff growth) so the deferral is visible in the PR thread — never drop one silently. - Critical-only mode: when `feedback.md` contains a - `Deferred non-Critical feedback` section, the PR has already completed five - suggestion-capable, change-producing rounds. That section is an audit record, + `Deferred non-Critical feedback` section, the workflow's deterministic brake + has engaged — the PR has completed five suggestion-capable, change-producing + rounds, or its diff has grown past the counting window's net-growth budget + (source and test lines are budgeted separately; the section's preamble names + the cause). That section is an audit record, not work: do not modify code, resolve threads, or write comment replies for those items. Everything rendered in the actionable sections IS in scope — the deterministic filter defers the automated reviewer's non-Critical diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js index 02b428e3d45..deb3fdb457e 100644 --- a/scripts/tests/qwen-autofix-workflow.test.js +++ b/scripts/tests/qwen-autofix-workflow.test.js @@ -5222,20 +5222,29 @@ exit 1 '[[ "${ROUND}" -ge "${CRITICAL_ONLY_AFTER_ROUND}" ]]', ); const modeBlock = prepareBranchAndFeedbackStep.match( - /(CRITICAL_ONLY='false'\n\s+if \[\[ "\$\{ROUND\}" -ge "\$\{CRITICAL_ONLY_AFTER_ROUND\}" \]\]; then\n\s+CRITICAL_ONLY='true'\n\s+fi)/, + /(CRITICAL_ONLY='false'\n\s+CRITICAL_ONLY_ROUNDS='false'\n\s+CRITICAL_ONLY_GROWTH='false'\n\s+if \[\[ "\$\{ROUND\}" -ge "\$\{CRITICAL_ONLY_AFTER_ROUND\}" \]\]; then\n\s+CRITICAL_ONLY='true'\n\s+CRITICAL_ONLY_ROUNDS='true'\n\s+fi\n\s+if \[\[ "\$\{GROWTH_SRC\}" -gt "\$\{GROWTH_BUDGET_SRC_LINES\}" \|\| "\$\{GROWTH_TEST\}" -gt "\$\{GROWTH_BUDGET_TEST_LINES\}" \]\]; then\n\s+CRITICAL_ONLY='true'\n\s+CRITICAL_ONLY_GROWTH='true'\n\s+fi)/, )?.[1]; expect(modeBlock).toBeTruthy(); - const modeAt = (round) => + const modeAt = (round, growthSrc = 0, growthTest = 0) => execFileSync( 'bash', [ '-c', - `ROUND=${round}\nCRITICAL_ONLY_AFTER_ROUND=5\n${modeBlock}\nprintf '%s' "$CRITICAL_ONLY"`, + `ROUND=${round}\nCRITICAL_ONLY_AFTER_ROUND=5\nGROWTH_SRC=${growthSrc}\nGROWTH_TEST=${growthTest}\nGROWTH_BUDGET_SRC_LINES=400\nGROWTH_BUDGET_TEST_LINES=400\n${modeBlock}\nprintf '%s %s %s' "$CRITICAL_ONLY" "$CRITICAL_ONLY_ROUNDS" "$CRITICAL_ONLY_GROWTH"`, ], { encoding: 'utf8' }, ); - expect(modeAt(4)).toBe('false'); - expect(modeAt(5)).toBe('true'); + expect(modeAt(4)).toBe('false false false'); + expect(modeAt(5)).toBe('true true false'); + // The growth brake trips the SAME mode before the round threshold. AT + // budget is within budget (exclusive boundary), either dimension alone + // trips, both causes can hold at once, and a shrinking window (negative + // growth) never engages. + expect(modeAt(0, 400, 400)).toBe('false false false'); + expect(modeAt(0, 401, 0)).toBe('true false true'); + expect(modeAt(0, 0, 401)).toBe('true false true'); + expect(modeAt(5, 401, 0)).toBe('true true true'); + expect(modeAt(0, -900, -900)).toBe('false false false'); // Once the boundary is crossed, only an explicit Critical inline finding // or a formal changes-requested review is actionable. Suggestion and @@ -5961,6 +5970,173 @@ exit 1 expect(skill).toContain('Deferred non-Critical feedback'); }); + it('anchors a per-window growth baseline and splits src/test nets against a real repo', () => { + // Budgets are repo-variable tunables with a sanitize fallback at the + // read site, mirroring the scan budgets. + expect(workflow).toContain( + "GROWTH_BUDGET_SRC_LINES: '${{ vars.QWEN_AUTOFIX_GROWTH_BUDGET_SRC_LINES || 400 }}'", + ); + expect(workflow).toContain( + "GROWTH_BUDGET_TEST_LINES: '${{ vars.QWEN_AUTOFIX_GROWTH_BUDGET_TEST_LINES || 400 }}'", + ); + expect(prepareBranchAndFeedbackStep).toContain( + '[[ ! "${GROWTH_BUDGET_SRC_LINES}" =~ ^[0-9]{1,7}$ ]]', + ); + expect(prepareBranchAndFeedbackStep).toContain( + '[[ ! "${GROWTH_BUDGET_TEST_LINES}" =~ ^[0-9]{1,7}$ ]]', + ); + + // Measurement: replay the real block against a real repo. Test lines are + // *.test.* / *.spec.* files, __snapshots__/, test-utils/, and + // integration-tests/ (by DIRECTORY, not file naming); binary files count + // as zero; deletions subtract. + const measureBlock = prepareBranchAndFeedbackStep.match( + /(# Binary files report[\s\S]*?NET_SRC=\$\(\( NET_TOTAL - NET_TEST \)\))/, + )?.[1]; + expect(measureBlock).toBeTruthy(); + const dir = mkdtempSync(join(tmpdir(), 'autofix-growth-')); + try { + const git = (...args) => + execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8' }); + git('init', '-q', '-b', 'main'); + git('config', 'user.email', 'test@test'); + git('config', 'user.name', 'test'); + mkdirSync(join(dir, 'src')); + writeFileSync(join(dir, 'src', 'app.ts'), 'a1\na2\na3\n'); + writeFileSync(join(dir, 'src', 'app.test.ts'), 't1\nt2\n'); + git('add', '-A'); + git('commit', '-qm', 'base'); + git('checkout', '-qb', 'pr'); + mkdirSync(join(dir, 'src', '__snapshots__')); + mkdirSync(join(dir, 'src', 'test-utils')); + mkdirSync(join(dir, 'integration-tests')); + mkdirSync(join(dir, 'assets')); + // src: full rewrite, +7/-3 = net +4 + writeFileSync(join(dir, 'src', 'app.ts'), 'b1\nb2\nb3\nb4\nb5\nb6\nb7\n'); + // tests: +3 appended, +5 spec, +4 snapshot, +2 test-utils, +2 + // integration-tests helper (a non-test filename proves the directory + // pathspec) = net +16 + writeFileSync(join(dir, 'src', 'app.test.ts'), 't1\nt2\nt3\nt4\nt5\n'); + writeFileSync(join(dir, 'src', 'util.spec.ts'), 's1\ns2\ns3\ns4\ns5\n'); + writeFileSync( + join(dir, 'src', '__snapshots__', 'app.snap'), + 'n1\nn2\nn3\nn4\n', + ); + writeFileSync(join(dir, 'src', 'test-utils', 'helper.ts'), 'h1\nh2\n'); + writeFileSync(join(dir, 'integration-tests', 'helper.ts'), 'i1\ni2\n'); + writeFileSync( + join(dir, 'assets', 'logo.bin'), + Buffer.from([0x00, 0x01, 0x02, 0x00]), + ); + git('add', '-A'); + git('commit', '-qm', 'pr'); + git('update-ref', 'refs/remotes/origin/main', 'main'); + const measured = execFileSync( + 'bash', + [ + '-c', + `set -e\n${measureBlock}\nprintf '%s %s %s' "$NET_TOTAL" "$NET_TEST" "$NET_SRC"`, + ], + { encoding: 'utf8', cwd: dir }, + ); + expect(measured).toBe('20 16 4'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + + // Baseline parse: bot-only, window-keyed, FIRST-wins — a duplicate + // marker in the same window cannot move an anchored baseline, a spoofed + // marker from another login is ignored, negative nets round-trip, and a + // window with no marker yields empty (this round anchors it). + const baselineJq = prepareBranchAndFeedbackStep.match( + /GROWTH_BASELINE="\$\(jq -r --arg ab "\$\{AUTOFIX_BOT\}" --arg key "\$\{LIVE_REARM_KEY\}" '([\s\S]*?)' "\$\{WORKDIR\}\/ic\.json"\)"/, + )?.[1]; + expect(baselineJq).toBeTruthy(); + const baselineComments = [ + { + user: { login: 'mallory' }, + created_at: '2026-01-01T00:00:00Z', + body: '', + }, + { + user: { login: 'qwen-code-dev-bot' }, + created_at: '2026-01-01T01:00:00Z', + body: 'report\n\n\n', + }, + { + user: { login: 'qwen-code-dev-bot' }, + created_at: '2026-01-01T02:00:00Z', + body: 'report\n\n', + }, + { + user: { login: 'qwen-code-dev-bot' }, + created_at: '2026-01-01T03:00:00Z', + body: 'report\n\n', + }, + ]; + const baselineFor = (key) => + execFileSync( + 'jq', + [ + '-r', + '--arg', + 'ab', + 'qwen-code-dev-bot', + '--arg', + 'key', + key, + baselineJq, + ], + { encoding: 'utf8', input: JSON.stringify(baselineComments) }, + ).trimEnd(); + expect(baselineFor('WIN1')).toBe('50 -60'); + expect(baselineFor('WIN2')).toBe(''); + + // The baseline marker is written into this window's FIRST report only + // (pushed and no-op branches), rides the same comment as autofix-eval so + // every feedback filter already excludes it, and never touches the + // POSITIONAL autofix-eval parsers. Exactly one more occurrence exists: + // the prepare-side scan() parse. + expect(workflow.split('', + ).length - 1, + ).toBe(2); + expect( + pushAndReportStep.split(`if [[ "\${GROWTH_BASE_NEW}" == 'true' ]]; then`) + .length - 1, + ).toBe(2); + expect(pushAndReportStep).toContain( + "GROWTH_BASE_NEW: '${{ steps.prepare.outputs.growth_base_new }}'", + ); + expect(pushAndReportStep).toContain( + "GROWTH_BASE_SRC: '${{ steps.prepare.outputs.growth_base_src }}'", + ); + expect(pushAndReportStep).toContain( + "GROWTH_BASE_TEST: '${{ steps.prepare.outputs.growth_base_test }}'", + ); + expect(prepareBranchAndFeedbackStep).toContain( + 'growth_base_new=${GROWTH_BASE_NEW}', + ); + expect(prepareBranchAndFeedbackStep).toContain( + 'growth_base_src=${BASE_SRC}', + ); + expect(prepareBranchAndFeedbackStep).toContain( + 'growth_base_test=${BASE_TEST}', + ); + + // The deferred preamble names the actual cause — a maintainer reading + // "after five rounds" on a round-2 PR that tripped the growth budget + // would reasonably conclude the brake misfired. + expect(prepareBranchAndFeedbackStep).toContain( + 'Critical-only mode is active: ${CAUSE_EN}', + ); + expect(prepareBranchAndFeedbackStep).toContain( + '已进入仅处理 Critical 的模式:${CAUSE_ZH}', + ); + }); + it('requires the address path to run verification and record it as evidence', () => { // Observed: #7408 committed a fix with a TS error the gate then rejected, // while its summary claimed "verified all 3 commits". A soft "run the From f5023d1b77164f2bd26f941a850f9535c8e2fda1 Mon Sep 17 00:00:00 2001 From: verify Date: Wed, 12 Aug 2026 14:32:39 +0800 Subject: [PATCH 2/8] feat(autofix): exclude mechanical churn from the growth measurement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lockfiles (root and nested package-lock.json, npm-shrinkwrap.json) and the regenerated settings schema arrive hundreds of lines at a time from a single command and are skimmed rather than reviewed, so counting them would burn the source budget on churn that carries no review burden. The exclusion list names generated artifacts exactly — a broad glob would silently exempt hand-written files from the budget. The fixture test now proves a root lockfile ('**/' glob-magic at depth zero), a nested one, and the exact schema path all stay out of the measured nets. --- .github/workflows/qwen-autofix.yml | 14 +++++++++-- scripts/tests/qwen-autofix-workflow.test.js | 27 ++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index 39ed6ac7785..e9f400865af 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -157,7 +157,9 @@ env: # and cannot be tightened on tests without also strangling source fixes. # Test lines are *.test.*/*.spec.* files, __snapshots__/, test-utils/, and # integration-tests/ (the pathspec lives in the prepare step); source is - # everything else. Either budget tripping engages the brake. + # everything else, minus mechanical churn (lockfiles and the regenerated + # settings schema) that is skimmed rather than reviewed. Either budget + # tripping engages the brake. # TUNABLE WITHOUT A CODE CHANGE like the scan budgets above; a malformed # value falls back to its default at the read site. GROWTH_BUDGET_SRC_LINES: '${{ vars.QWEN_AUTOFIX_GROWTH_BUDGET_SRC_LINES || 400 }}' @@ -4061,7 +4063,15 @@ jobs: # Binary files report "-" in numstat; count them as 0 lines. sum_numstat() { awk '{ if ($1 != "-") a += $1; if ($2 != "-") d += $2 } END { print a - d + 0 }'; } TEST_PATHSPEC=(':(glob)**/*.test.*' ':(glob)**/*.spec.*' ':(glob)**/__snapshots__/**' ':(glob)**/test-utils/**' ':(glob)integration-tests/**') - NET_TOTAL="$(git diff --numstat origin/main...HEAD | sum_numstat)" + # 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. None of + # these can match TEST_PATHSPEC, so NET_SRC stays a clean subtraction. + GENERATED_EXCLUDES=(':(exclude,glob)**/package-lock.json' ':(exclude,glob)**/npm-shrinkwrap.json' ':(exclude)packages/vscode-ide-companion/schemas/settings.schema.json') + NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" | sum_numstat)" NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" | sum_numstat)" NET_SRC=$(( NET_TOTAL - NET_TEST )) GROWTH_BASELINE="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${LIVE_REARM_KEY}" ' diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js index deb3fdb457e..b827ce8a11f 100644 --- a/scripts/tests/qwen-autofix-workflow.test.js +++ b/scripts/tests/qwen-autofix-workflow.test.js @@ -5989,7 +5989,8 @@ exit 1 // Measurement: replay the real block against a real repo. Test lines are // *.test.* / *.spec.* files, __snapshots__/, test-utils/, and // integration-tests/ (by DIRECTORY, not file naming); binary files count - // as zero; deletions subtract. + // as zero; deletions subtract; mechanical churn (root and nested + // lockfiles, the regenerated settings schema) never burns the budget. const measureBlock = prepareBranchAndFeedbackStep.match( /(# Binary files report[\s\S]*?NET_SRC=\$\(\( NET_TOTAL - NET_TEST \)\))/, )?.[1]; @@ -6028,6 +6029,27 @@ exit 1 join(dir, 'assets', 'logo.bin'), Buffer.from([0x00, 0x01, 0x02, 0x00]), ); + // Mechanical churn: a ROOT lockfile (proves '**/' glob-magic matches + // at depth zero), a nested one, and the exact generated-schema path — + // all excluded, so none of them shift the expected nets below. + writeFileSync(join(dir, 'package-lock.json'), 'l1\nl2\nl3\nl4\nl5\n'); + mkdirSync(join(dir, 'packages', 'vscode-ide-companion', 'schemas'), { + recursive: true, + }); + writeFileSync( + join(dir, 'packages', 'vscode-ide-companion', 'package-lock.json'), + 'm1\nm2\nm3\n', + ); + writeFileSync( + join( + dir, + 'packages', + 'vscode-ide-companion', + 'schemas', + 'settings.schema.json', + ), + 'g1\ng2\ng3\ng4\n', + ); git('add', '-A'); git('commit', '-qm', 'pr'); git('update-ref', 'refs/remotes/origin/main', 'main'); @@ -6040,6 +6062,9 @@ exit 1 { encoding: 'utf8', cwd: dir }, ); expect(measured).toBe('20 16 4'); + expect(measureBlock).toContain( + "GENERATED_EXCLUDES=(':(exclude,glob)**/package-lock.json' ':(exclude,glob)**/npm-shrinkwrap.json' ':(exclude)packages/vscode-ide-companion/schemas/settings.schema.json')", + ); } finally { rmSync(dir, { recursive: true, force: true }); } From 50c1c25e69a23afb572dd264a9a1b32e6c552d14 Mon Sep 17 00:00:00 2001 From: verify Date: Wed, 12 Aug 2026 18:09:17 +0800 Subject: [PATCH 3/8] fix(autofix): harden growth-brake measurement per review round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Anchor the baseline marker under the window key prepare READ it with (LIVE_REARM_KEY), not the matrix WINDOW: supersede-exempt conflict rounds could write the live window's first marker under a dead key, letting the round's pushed growth escape the budget for the window. - Apply GENERATED_EXCLUDES to the test-side measurement too: a lockfile under integration-tests/ would otherwise be excluded from NET_TOTAL but counted in NET_TEST, corrupting the NET_SRC subtraction. - Count __tests__/ as test code, matching AGENTS.md's triage rule and repo-hygiene's PROD_EXCLUDE; suffix-less helpers there were charged to the source budget. - Reject zero-padded budget values in the sanitize guard: [[ -gt ]] parses them as octal ('0400' brakes 144 lines early, '0900' silently disables the brake). - Render signed growth values without a hardcoded '+' ('+-120' read like a misfire in the cause preamble, both languages). - Fail open to zero when the three-dot diff has no merge base (orphan- history branches via fork takeover/adoption), mirroring the merge-tree conflict probe's fail-open. - Retry the report post (3 attempts): that one comment carries the round's entire persisted state — watermark, round, redcheck head, and now the growth baseline — and the push has already landed by then. - Behaviorally replay the sanitize fallback and the cause construction (three engagement shapes, both languages, sign rendering) instead of text-pinning them; extend the measurement fixture with a __tests__ helper and a lockfile under a test directory. --- .github/workflows/qwen-autofix.yml | 79 ++++++++++++++--- scripts/tests/qwen-autofix-workflow.test.js | 94 ++++++++++++++++++--- 2 files changed, 148 insertions(+), 25 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index e9f400865af..873ff7335f6 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -4052,27 +4052,44 @@ jobs: # Growth-triggered Critical-only reuses the round brake's entire # deferral machinery below; the human batch budget stays # round-scoped, so maintainer feedback flows exactly as today. - if [[ ! "${GROWTH_BUDGET_SRC_LINES}" =~ ^[0-9]{1,7}$ ]]; then - echo "::warning::GROWTH_BUDGET_SRC_LINES='${GROWTH_BUDGET_SRC_LINES}' is not a line count; using 400" + # Leading zeros are rejected, not just non-digits: bash [[ -gt ]] + # reads a zero-padded operand as OCTAL, so '0400' would compare as + # 256 (the brake fires early) and '0900' raises "value too great + # for base" inside [[ ]], which under an if-condition silently + # evaluates false — the brake never engages. Both violate the + # documented fallback promise, so pad-shaped values fall back too. + if [[ ! "${GROWTH_BUDGET_SRC_LINES}" =~ ^(0|[1-9][0-9]{0,6})$ ]]; then + echo "::warning::GROWTH_BUDGET_SRC_LINES='${GROWTH_BUDGET_SRC_LINES}' is not a plain line count; using 400" GROWTH_BUDGET_SRC_LINES=400 fi - if [[ ! "${GROWTH_BUDGET_TEST_LINES}" =~ ^[0-9]{1,7}$ ]]; then - echo "::warning::GROWTH_BUDGET_TEST_LINES='${GROWTH_BUDGET_TEST_LINES}' is not a line count; using 400" + if [[ ! "${GROWTH_BUDGET_TEST_LINES}" =~ ^(0|[1-9][0-9]{0,6})$ ]]; then + echo "::warning::GROWTH_BUDGET_TEST_LINES='${GROWTH_BUDGET_TEST_LINES}' is not a plain line count; using 400" GROWTH_BUDGET_TEST_LINES=400 fi # Binary files report "-" in numstat; count them as 0 lines. sum_numstat() { awk '{ if ($1 != "-") a += $1; if ($2 != "-") d += $2 } END { print a - d + 0 }'; } - TEST_PATHSPEC=(':(glob)**/*.test.*' ':(glob)**/*.spec.*' ':(glob)**/__snapshots__/**' ':(glob)**/test-utils/**' ':(glob)integration-tests/**') + # __tests__/ is part of the repo's existing test-file definition + # (AGENTS.md's triage line-counting rule, repo-hygiene's + # PROD_EXCLUDE): helpers under it without a .test./.spec. suffix + # are still test code, not source. + TEST_PATHSPEC=(':(glob)**/*.test.*' ':(glob)**/*.spec.*' ':(glob)**/__snapshots__/**' ':(glob)**/__tests__/**' ':(glob)**/test-utils/**' ':(glob)integration-tests/**') # 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. None of - # these can match TEST_PATHSPEC, so NET_SRC stays a clean subtraction. + # 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. GENERATED_EXCLUDES=(':(exclude,glob)**/package-lock.json' ':(exclude,glob)**/npm-shrinkwrap.json' ':(exclude)packages/vscode-ide-companion/schemas/settings.schema.json') - NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" | sum_numstat)" - NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" | sum_numstat)" + # An orphan-history branch (fork takeover / adoption admits one — + # nothing on this job's fetch requires a common ancestor) has no + # merge base: the three-dot diff exits 128. Fail OPEN to zero like + # the merge-tree conflict probe above — an unmeasurable PR skips + # the brake rather than dying red at measurement every round. + NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TOTAL=0 + NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TEST=0 NET_SRC=$(( NET_TOTAL - NET_TEST )) GROWTH_BASELINE="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${LIVE_REARM_KEY}" ' [ .[] | select((.user.login // "") == $ab) | . as $c | ($c.body // "") @@ -4095,6 +4112,14 @@ jobs: echo "growth_base_new=${GROWTH_BASE_NEW}" echo "growth_base_src=${BASE_SRC}" echo "growth_base_test=${BASE_TEST}" + # The key the baseline was READ under. The report must write the + # marker under this same key, not the matrix WINDOW: a conflict + # round is exempt from the supersede discard, so it can run with + # a stale WINDOW after a re-arm — a marker written under that + # dead key would be invisible to every later read and the + # round's pushed growth would escape the budget for the rest of + # the live window. + echo "growth_base_win=${LIVE_REARM_KEY}" } >> "${GITHUB_OUTPUT}" echo "📏 net diff src ${NET_SRC} / test ${NET_TEST} lines (window baseline ${BASE_SRC}/${BASE_TEST}, growth ${GROWTH_SRC}/${GROWTH_TEST}, budgets ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" @@ -4183,8 +4208,11 @@ jobs: # Name the cause(s) precisely: a maintainer reading "after five # rounds" on a round-2 PR that tripped the GROWTH budget would # reasonably conclude the brake misfired. - GROWTH_CLAUSE_EN="the PR's diff grew src +${GROWTH_SRC} / test +${GROWTH_TEST} net lines beyond this counting window's baseline (budgets: ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" - GROWTH_CLAUSE_ZH="本计数窗口内 diff 净增长已达 源码 +${GROWTH_SRC} / 测试 +${GROWTH_TEST} 行(预算 ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" + # No literal '+' prefix: the values are signed (either dimension + # can shrink while the other trips the brake), and '+-120' in + # the cause line reads like a misfire to exactly its audience. + GROWTH_CLAUSE_EN="the PR's diff grew src ${GROWTH_SRC} / test ${GROWTH_TEST} net lines beyond this counting window's baseline (budgets: ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" + GROWTH_CLAUSE_ZH="本计数窗口内 diff 净增长已达 源码 ${GROWTH_SRC} / 测试 ${GROWTH_TEST} 行(预算 ${GROWTH_BUDGET_SRC_LINES}/${GROWTH_BUDGET_TEST_LINES})" ROUNDS_CLAUSE_EN="${CRITICAL_ONLY_AFTER_ROUND} change-producing rounds are complete" ROUNDS_CLAUSE_ZH="已完成 ${CRITICAL_ONLY_AFTER_ROUND} 个产生改动的轮次" if [[ "${CRITICAL_ONLY_ROUNDS}" == 'true' && "${CRITICAL_ONLY_GROWTH}" == 'true' ]]; then @@ -4836,6 +4864,11 @@ jobs: GROWTH_BASE_NEW: '${{ steps.prepare.outputs.growth_base_new }}' GROWTH_BASE_SRC: '${{ steps.prepare.outputs.growth_base_src }}' GROWTH_BASE_TEST: '${{ steps.prepare.outputs.growth_base_test }}' + # The window key prepare READ the baseline under (LIVE_REARM_KEY), + # not the matrix WINDOW: conflict rounds are supersede-exempt and + # can report under a stale WINDOW after a re-arm; the marker must + # land under the key later reads will use. + GROWTH_BASE_WIN: '${{ steps.prepare.outputs.growth_base_win }}' run: |- # The head the agent actually evaluated — captured in prepare before # any mutation, not the report-time remote head (which can move @@ -5104,7 +5137,7 @@ jobs: echo "" echo "" if [[ "${GROWTH_BASE_NEW}" == 'true' ]]; then - echo "" + echo "" fi } > "${WORKDIR}/report.md" STATUS="pushed (round ${NEXT_ROUND}/${MAX_ROUNDS})" @@ -5128,13 +5161,31 @@ jobs: echo "" echo "" if [[ "${GROWTH_BASE_NEW}" == 'true' ]]; then - echo "" + echo "" fi } > "${WORKDIR}/report.md" STATUS="no action needed" fi - gh pr comment "${PR}" --repo "${REPO}" --body-file "${WORKDIR}/report.md" + # 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). + REPORT_POSTED='false' + for attempt in 1 2 3; do + if gh pr comment "${PR}" --repo "${REPO}" --body-file "${WORKDIR}/report.md"; then + REPORT_POSTED='true' + break + fi + echo "::warning::report post attempt ${attempt} failed for PR #${PR}; retrying" + sleep 10 + done + [[ "${REPORT_POSTED}" == 'true' ]] || exit 1 # Takeover milestone digest — roughly every 10 rounds. The takeover # cap (100) bounds runaway but says nothing about when a human diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js index b827ce8a11f..cff86750a8e 100644 --- a/scripts/tests/qwen-autofix-workflow.test.js +++ b/scripts/tests/qwen-autofix-workflow.test.js @@ -5979,18 +5979,39 @@ exit 1 expect(workflow).toContain( "GROWTH_BUDGET_TEST_LINES: '${{ vars.QWEN_AUTOFIX_GROWTH_BUDGET_TEST_LINES || 400 }}'", ); - expect(prepareBranchAndFeedbackStep).toContain( - '[[ ! "${GROWTH_BUDGET_SRC_LINES}" =~ ^[0-9]{1,7}$ ]]', - ); - expect(prepareBranchAndFeedbackStep).toContain( - '[[ ! "${GROWTH_BUDGET_TEST_LINES}" =~ ^[0-9]{1,7}$ ]]', - ); + // The sanitize fallback is REPLAYED, not just pinned: it is the sole + // protection against a malformed repo variable reaching the octal- + // parsing [[ -gt ]] comparisons, in both failure directions. + const sanitizeBlock = prepareBranchAndFeedbackStep.match( + /(if \[\[ ! "\$\{GROWTH_BUDGET_SRC_LINES\}"[\s\S]*?GROWTH_BUDGET_TEST_LINES=400\n\s+fi)/, + )?.[1]; + expect(sanitizeBlock).toBeTruthy(); + // Last line only: the fallback path also emits its ::warning:: lines. + const sanitized = (src, test) => + execFileSync( + 'bash', + [ + '-c', + `GROWTH_BUDGET_SRC_LINES='${src}'\nGROWTH_BUDGET_TEST_LINES='${test}'\n${sanitizeBlock}\nprintf '\\n%s %s' "$GROWTH_BUDGET_SRC_LINES" "$GROWTH_BUDGET_TEST_LINES"`, + ], + { encoding: 'utf8' }, + ) + .split('\n') + .pop(); + // Plain counts (zero included) pass through untouched. + expect(sanitized('400', '0')).toBe('400 0'); + // Garbage falls back… + expect(sanitized('400abc', 'twelve')).toBe('400 400'); + // …and so do zero-padded values: bash [[ -gt ]] would read '0400' as + // octal 256 and raise on '0900', silently disabling the brake. + expect(sanitized('0400', '0900')).toBe('400 400'); // Measurement: replay the real block against a real repo. Test lines are - // *.test.* / *.spec.* files, __snapshots__/, test-utils/, and + // *.test.* / *.spec.* files, __snapshots__/, __tests__/, test-utils/, and // integration-tests/ (by DIRECTORY, not file naming); binary files count // as zero; deletions subtract; mechanical churn (root and nested - // lockfiles, the regenerated settings schema) never burns the budget. + // lockfiles, the regenerated settings schema) never burns the budget — + // on EITHER side of the src/test split, even under a test directory. const measureBlock = prepareBranchAndFeedbackStep.match( /(# Binary files report[\s\S]*?NET_SRC=\$\(\( NET_TOTAL - NET_TEST \)\))/, )?.[1]; @@ -6016,7 +6037,7 @@ exit 1 writeFileSync(join(dir, 'src', 'app.ts'), 'b1\nb2\nb3\nb4\nb5\nb6\nb7\n'); // tests: +3 appended, +5 spec, +4 snapshot, +2 test-utils, +2 // integration-tests helper (a non-test filename proves the directory - // pathspec) = net +16 + // pathspec) plus +2 __tests__ setup below = net +18 writeFileSync(join(dir, 'src', 'app.test.ts'), 't1\nt2\nt3\nt4\nt5\n'); writeFileSync(join(dir, 'src', 'util.spec.ts'), 's1\ns2\ns3\ns4\ns5\n'); writeFileSync( @@ -6025,6 +6046,15 @@ exit 1 ); writeFileSync(join(dir, 'src', 'test-utils', 'helper.ts'), 'h1\nh2\n'); writeFileSync(join(dir, 'integration-tests', 'helper.ts'), 'i1\ni2\n'); + // __tests__/ helper without a .test./.spec. suffix is test code… + mkdirSync(join(dir, 'src', '__tests__')); + writeFileSync(join(dir, 'src', '__tests__', 'setup.ts'), 'u1\nu2\n'); + // …and a lockfile under a test directory is mechanical churn on BOTH + // sides of the split, or NET_SRC would be corrupted by the subtraction. + writeFileSync( + join(dir, 'integration-tests', 'package-lock.json'), + 'k1\nk2\nk3\nk4\nk5\nk6\n', + ); writeFileSync( join(dir, 'assets', 'logo.bin'), Buffer.from([0x00, 0x01, 0x02, 0x00]), @@ -6061,7 +6091,7 @@ exit 1 ], { encoding: 'utf8', cwd: dir }, ); - expect(measured).toBe('20 16 4'); + expect(measured).toBe('22 18 4'); expect(measureBlock).toContain( "GENERATED_EXCLUDES=(':(exclude,glob)**/package-lock.json' ':(exclude,glob)**/npm-shrinkwrap.json' ':(exclude)packages/vscode-ide-companion/schemas/settings.schema.json')", ); @@ -6125,7 +6155,7 @@ exit 1 expect(workflow.split('', + '', ).length - 1, ).toBe(2); expect( @@ -6141,6 +6171,14 @@ exit 1 expect(pushAndReportStep).toContain( "GROWTH_BASE_TEST: '${{ steps.prepare.outputs.growth_base_test }}'", ); + // The marker is written under the key the baseline was READ under + // (LIVE_REARM_KEY), not the matrix WINDOW: a supersede-exempt conflict + // round can report under a stale WINDOW after a re-arm, and a marker + // under that dead key would hide the round's pushed growth from every + // later read in the live window. + expect(pushAndReportStep).toContain( + "GROWTH_BASE_WIN: '${{ steps.prepare.outputs.growth_base_win }}'", + ); expect(prepareBranchAndFeedbackStep).toContain( 'growth_base_new=${GROWTH_BASE_NEW}', ); @@ -6150,6 +6188,9 @@ exit 1 expect(prepareBranchAndFeedbackStep).toContain( 'growth_base_test=${BASE_TEST}', ); + expect(prepareBranchAndFeedbackStep).toContain( + 'growth_base_win=${LIVE_REARM_KEY}', + ); // The deferred preamble names the actual cause — a maintainer reading // "after five rounds" on a round-2 PR that tripped the growth budget @@ -6160,6 +6201,37 @@ exit 1 expect(prepareBranchAndFeedbackStep).toContain( '已进入仅处理 Critical 的模式:${CAUSE_ZH}', ); + // The cause construction is REPLAYED across the three engagement shapes, + // in both languages: a growth-only trip must never announce the round + // threshold, signs render naturally (no '+-120'), and EN/ZH always name + // the same cause. + const causeBlock = prepareBranchAndFeedbackStep.match( + /(GROWTH_CLAUSE_EN="the PR's diff grew[\s\S]*?CAUSE_ZH="\$\{ROUNDS_CLAUSE_ZH\}"\n\s+fi)/, + )?.[1]; + expect(causeBlock).toBeTruthy(); + const causeFor = (rounds, growth, growthSrc, growthTest) => + execFileSync( + 'bash', + [ + '-c', + `CRITICAL_ONLY_ROUNDS=${rounds}\nCRITICAL_ONLY_GROWTH=${growth}\nGROWTH_SRC=${growthSrc}\nGROWTH_TEST=${growthTest}\nGROWTH_BUDGET_SRC_LINES=400\nGROWTH_BUDGET_TEST_LINES=400\nCRITICAL_ONLY_AFTER_ROUND=5\n${causeBlock}\nprintf '%s\\n%s' "$CAUSE_EN" "$CAUSE_ZH"`, + ], + { encoding: 'utf8' }, + ).split('\n'); + const growthOnly = causeFor('false', 'true', -120, 500); + expect(growthOnly[0]).toContain('src -120 / test 500'); + expect(growthOnly[0]).not.toContain('rounds are complete'); + expect(growthOnly[0]).not.toContain('+-'); + expect(growthOnly[1]).toContain('源码 -120 / 测试 500'); + expect(growthOnly[1]).not.toContain('轮次'); + const roundsOnly = causeFor('true', 'false', 0, 0); + expect(roundsOnly[0]).toBe('5 change-producing rounds are complete'); + expect(roundsOnly[0]).not.toContain('diff grew'); + expect(roundsOnly[1]).toContain('已完成 5 个产生改动的轮次'); + const both = causeFor('true', 'true', 900, 20); + expect(both[0]).toContain('rounds are complete and'); + expect(both[0]).toContain('src 900 / test 20'); + expect(both[1]).toContain('轮次,且'); }); it('requires the address path to run verification and record it as evidence', () => { From 92a075b612e40fc26fbe383502230afda7632d13 Mon Sep 17 00:00:00 2001 From: verify Date: Wed, 12 Aug 2026 20:54:45 +0800 Subject: [PATCH 4/8] fix(autofix): close the round-2 review findings on the growth brake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spell the growth marker's window field key= instead of win=: the same report comment can legitimately carry a different window key than its autofix-eval marker (supersede-exempt conflict round after a re-arm), and three censuses attribute comments to windows by the whole-body substring win= -->, which would double-attribute that comment to both windows (probe-flipped PRIOR_TIMEOUTS, WIN_HEADS, PRIOR_HEADS). A distinct token immunizes every such census without touching them. - Make the deferred preamble's batch-budget sentence conditional: the OVER_BUDGET census only builds spans in round-brake territory, so a growth-only engagement below the threshold now states that maintainer feedback flows unaffected instead of promising accounting the census cannot produce. - Special-case the report-post retry's final attempt: no trailing 'retrying' + 10s sleep before giving up. - Include __tests__/ in the env comment's test-line enumeration (the tunables doc must match the pathspec). - Replay coverage for everything the mutation probes showed unpinned: the baseline wiring block (parseable/empty/malformed baselines), the no-merge-base fail-open (0/0/0 under -eo pipefail with the origin ref deleted), the report-post retry (single post on success; exactly three attempts, 'giving up', exit 1 on outage), the writer→scanner marker round-trip (negative src rendered from the real template and parsed back), and the budget-sentence branches in both languages. --- .github/workflows/qwen-autofix.yml | 42 ++++- scripts/tests/qwen-autofix-workflow.test.js | 162 +++++++++++++++++++- 2 files changed, 191 insertions(+), 13 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index 873ff7335f6..08c9a740763 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -155,7 +155,8 @@ env: # growth was 86% test lines — every round pins ever-more-marginal behavior; # #8276's was 78%), so a single budget is effectively spent by test growth # and cannot be tightened on tests without also strangling source fixes. - # Test lines are *.test.*/*.spec.* files, __snapshots__/, test-utils/, and + # Test lines are *.test.*/*.spec.* files, __snapshots__/, __tests__/, + # test-utils/, and # integration-tests/ (the pathspec lives in the prepare step); source is # everything else, minus mechanical churn (lockfiles and the regenerated # settings schema) that is skimmed rather than reviewed. Either budget @@ -4091,9 +4092,17 @@ jobs: NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TOTAL=0 NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TEST=0 NET_SRC=$(( NET_TOTAL - NET_TEST )) + # The marker's window field is spelled `key=`, NOT `win=`, on + # purpose: three censuses (PRIOR_TIMEOUTS, the milestone WIN_HEADS, + # the breaker PRIOR_HEADS) attribute comments to windows by the + # WHOLE-BODY substring `win= -->`, and this marker can + # legitimately carry a different window key than its comment's + # autofix-eval marker (a supersede-exempt conflict round reporting + # after a re-arm). Any token embedding "win=" would double-attribute + # that comment to both windows. GROWTH_BASELINE="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${LIVE_REARM_KEY}" ' [ .[] | select((.user.login // "") == $ab) | . as $c | ($c.body // "") - | [ scan("") ] | .[] + | [ scan("") ] | .[] | {src: (.[0] | tonumber), test: (.[1] | tonumber), win: .[2], at: ($c.created_at // "")} ] | map(select(.win == $key)) | sort_by(.at) | .[0] // empty | "\(.src) \(.test)"' "${WORKDIR}/ic.json")" @@ -4225,10 +4234,23 @@ jobs: CAUSE_EN="${ROUNDS_CLAUSE_EN}" CAUSE_ZH="${ROUNDS_CLAUSE_ZH}" fi + # The maintainer batch budget is enforced by the OVER_BUDGET + # census, whose spans exist only in round-brake territory + # (rounds past CRITICAL_ONLY_AFTER_ROUND). A growth-only + # engagement below the threshold has no enforceable budget, and + # the audit record must describe the policy actually in force — + # not promise accounting the census cannot produce. + if [[ "${CRITICAL_ONLY_ROUNDS}" == 'true' ]]; then + BUDGET_EN="Maintainer feedback is deferred only after its author has used ${CRITICAL_ONLY_HUMAN_BATCHES} regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below." + BUDGET_ZH="维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 ${CRITICAL_ONLY_HUMAN_BATCHES} 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。" + else + BUDGET_EN="Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after ${CRITICAL_ONLY_AFTER_ROUND} change-producing rounds)." + BUDGET_ZH="纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 ${CRITICAL_ONLY_AFTER_ROUND} 个产生改动的轮次后生效)。" + fi { echo '## Deferred non-Critical feedback' echo - echo "Critical-only mode is active: ${CAUSE_EN}. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used ${CRITICAL_ONLY_HUMAN_BATCHES} regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (\`@qwen-code /retry\` starts a fresh counting window.)" + echo "Critical-only mode is active: ${CAUSE_EN}. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. ${BUDGET_EN} (\`@qwen-code /retry\` starts a fresh counting window.)" echo jq -r --arg wm "${WATERMARK}" --arg rb "${REVIEW_BOT}" --arg ab "${AUTOFIX_BOT}" \ --arg pr_url "${PR_URL}" --argjson over "${OVER_BUDGET_AUTHORS}" ' @@ -4284,7 +4306,7 @@ jobs: echo '
' echo '中文说明' echo - echo "已进入仅处理 Critical 的模式:${CAUSE_ZH}。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 ${CRITICAL_ONLY_HUMAN_BATCHES} 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 \`@qwen-code /retry\` 可开启新的计数窗口。)" + echo "已进入仅处理 Critical 的模式:${CAUSE_ZH}。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。${BUDGET_ZH}(评论 \`@qwen-code /retry\` 可开启新的计数窗口。)" echo echo '
' } > "${WORKDIR}/deferred-feedback.md" @@ -5137,7 +5159,7 @@ jobs: echo "" echo "" if [[ "${GROWTH_BASE_NEW}" == 'true' ]]; then - echo "" + echo "" fi } > "${WORKDIR}/report.md" STATUS="pushed (round ${NEXT_ROUND}/${MAX_ROUNDS})" @@ -5161,7 +5183,7 @@ jobs: echo "" echo "" if [[ "${GROWTH_BASE_NEW}" == 'true' ]]; then - echo "" + echo "" fi } > "${WORKDIR}/report.md" STATUS="no action needed" @@ -5182,8 +5204,12 @@ jobs: REPORT_POSTED='true' break fi - echo "::warning::report post attempt ${attempt} failed for PR #${PR}; retrying" - sleep 10 + if [[ "${attempt}" == 3 ]]; then + echo "::error::report post failed ${attempt} times for PR #${PR}; giving up" + else + echo "::warning::report post attempt ${attempt} failed for PR #${PR}; retrying" + sleep 10 + fi done [[ "${REPORT_POSTED}" == 'true' ]] || exit 1 diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js index cff86750a8e..ff3c5331255 100644 --- a/scripts/tests/qwen-autofix-workflow.test.js +++ b/scripts/tests/qwen-autofix-workflow.test.js @@ -6092,6 +6092,20 @@ exit 1 { encoding: 'utf8', cwd: dir }, ); expect(measured).toBe('22 18 4'); + // Fail-open: an orphan-history branch has no merge base, the three-dot + // diff exits 128, and under the workflow's real shell options the + // block must still complete with zero nets (brake skipped) instead of + // killing the prepare step every round. + git('update-ref', '-d', 'refs/remotes/origin/main'); + const orphan = execFileSync( + 'bash', + [ + '-c', + `set -eo pipefail\n${measureBlock}\nprintf '%s %s %s' "$NET_TOTAL" "$NET_TEST" "$NET_SRC"`, + ], + { encoding: 'utf8', cwd: dir }, + ); + expect(orphan).toBe('0 0 0'); expect(measureBlock).toContain( "GENERATED_EXCLUDES=(':(exclude,glob)**/package-lock.json' ':(exclude,glob)**/npm-shrinkwrap.json' ':(exclude)packages/vscode-ide-companion/schemas/settings.schema.json')", ); @@ -6111,22 +6125,22 @@ exit 1 { user: { login: 'mallory' }, created_at: '2026-01-01T00:00:00Z', - body: '', + body: '', }, { user: { login: 'qwen-code-dev-bot' }, created_at: '2026-01-01T01:00:00Z', - body: 'report\n\n\n', + body: 'report\n\n\n', }, { user: { login: 'qwen-code-dev-bot' }, created_at: '2026-01-01T02:00:00Z', - body: 'report\n\n', + body: 'report\n\n', }, { user: { login: 'qwen-code-dev-bot' }, created_at: '2026-01-01T03:00:00Z', - body: 'report\n\n', + body: 'report\n\n', }, ]; const baselineFor = (key) => @@ -6155,7 +6169,7 @@ exit 1 expect(workflow.split('', + '', ).length - 1, ).toBe(2); expect( @@ -6232,6 +6246,144 @@ exit 1 expect(both[0]).toContain('rounds are complete and'); expect(both[0]).toContain('src 900 / test 20'); expect(both[1]).toContain('轮次,且'); + + // The batch-budget sentence must describe the policy actually in force: + // the OVER_BUDGET census only builds spans in round-brake territory, so + // a growth-only engagement has no enforceable budget and must say so. + const budgetBlock = prepareBranchAndFeedbackStep.match( + /(if \[\[ "\$\{CRITICAL_ONLY_ROUNDS\}" == 'true' \]\]; then\n\s+BUDGET_EN=[\s\S]*?BUDGET_ZH="纯增长[\s\S]*?fi)/, + )?.[1]; + expect(budgetBlock).toBeTruthy(); + const budgetFor = (rounds) => + execFileSync( + 'bash', + [ + '-c', + `CRITICAL_ONLY_ROUNDS=${rounds}\nCRITICAL_ONLY_HUMAN_BATCHES=2\nCRITICAL_ONLY_AFTER_ROUND=5\n${budgetBlock}\nprintf '%s\\n%s' "$BUDGET_EN" "$BUDGET_ZH"`, + ], + { encoding: 'utf8' }, + ).split('\n'); + const roundsBudget = budgetFor('true'); + expect(roundsBudget[0]).toContain('used 2 regular feedback batches'); + expect(roundsBudget[1]).toContain('2 批常规反馈预算'); + const growthBudget = budgetFor('false'); + expect(growthBudget[0]).toContain('continues to flow unaffected'); + expect(growthBudget[0]).not.toContain('named below'); + expect(growthBudget[1]).toContain('照常流动'); + + // Baseline wiring: the BASH_REMATCH split, fresh-anchor fallback, and + // growth subtractions — the producer (measurement/jq) and consumer + // (mode block) are replayed elsewhere; this replays the middle. + const wiringBlock = prepareBranchAndFeedbackStep.match( + /(GROWTH_BASE_NEW='false'[\s\S]*?GROWTH_TEST=\$\(\( NET_TEST - BASE_TEST \)\))/, + )?.[1]; + expect(wiringBlock).toBeTruthy(); + const wire = (baseline, netSrc, netTest) => + execFileSync( + 'bash', + [ + '-c', + `GROWTH_BASELINE='${baseline}'\nNET_SRC=${netSrc}\nNET_TEST=${netTest}\n${wiringBlock}\nprintf '%s %s %s %s %s' "$GROWTH_BASE_NEW" "$BASE_SRC" "$BASE_TEST" "$GROWTH_SRC" "$GROWTH_TEST"`, + ], + { encoding: 'utf8' }, + ); + // Parseable baseline: growth = net − base, marker not re-written. + expect(wire('50 -60', 120, 40)).toBe('false 50 -60 70 100'); + // Empty baseline: THIS round anchors — marker written, growth zero. + expect(wire('', 120, 40)).toBe('true 120 40 0 0'); + // Malformed baseline falls to the fresh anchor too. + expect(wire('garbage', 120, 40)).toBe('true 120 40 0 0'); + + // Writer↔scanner round-trip: render the report step's ACTUAL marker + // template (negative src, explicit window key) and require the prepare + // step's scan() to parse it back — the two sides are otherwise pinned + // only separately, so format drift would ship green while the baseline + // never parses in production. + const markerTemplate = pushAndReportStep.match( + /echo "()"/, + )?.[1]; + expect(markerTemplate).toBeTruthy(); + const rendered = execFileSync( + 'bash', + [ + '-c', + `GROWTH_BASE_SRC=-5\nGROWTH_BASE_TEST=0\nGROWTH_BASE_WIN=WINX\necho "${markerTemplate}"`, + ], + { encoding: 'utf8' }, + ).trim(); + const roundTrip = execFileSync( + 'jq', + [ + '-r', + '--arg', + 'ab', + 'qwen-code-dev-bot', + '--arg', + 'key', + 'WINX', + baselineJq, + ], + { + encoding: 'utf8', + input: JSON.stringify([ + { + user: { login: 'qwen-code-dev-bot' }, + created_at: '2026-01-01T00:00:00Z', + body: `report\n\n${rendered}`, + }, + ]), + }, + ).trimEnd(); + expect(roundTrip).toBe('-5 0'); + // The marker's window field is `key=`, never `win=`: three censuses + // attribute comments to windows by the whole-body substring + // `win= -->`, and this marker can carry a different window than + // its comment's autofix-eval marker. + expect(rendered).not.toContain('win='); + + // The report-post retry loop carries the round's entire persisted + // state: replay it with a stubbed gh — a success posts exactly once; + // a full outage attempts exactly three times, ends with "giving up" + // (not a fourth "retrying"), and fails the step. + const retryBlock = pushAndReportStep.match( + /(REPORT_POSTED='false'[\s\S]*?\[\[ "\$\{REPORT_POSTED\}" == 'true' \]\] \|\| exit 1)/, + )?.[1]; + expect(retryBlock).toBeTruthy(); + const retry = (ghExit) => { + const calls = mkdtempSync(join(tmpdir(), 'autofix-retry-')); + const res = spawnSync( + 'bash', + [ + '-c', + [ + 'set -eo pipefail', + 'PR=1 REPO=o/r WORKDIR=.', + 'sleep() { :; }', + `gh() { echo x >> "$1/calls"; return ${ghExit}; }`.replace( + '$1', + calls, + ), + retryBlock, + 'echo POSTED_OK', + ].join('\n'), + ], + { encoding: 'utf8' }, + ); + const count = existsSync(join(calls, 'calls')) + ? readFileSync(join(calls, 'calls'), 'utf8').split('\n').filter(Boolean) + .length + : 0; + rmSync(calls, { recursive: true, force: true }); + return { out: `${res.stdout}\n${res.stderr}`, status: res.status, count }; + }; + const posted = retry(0); + expect(posted.count).toBe(1); + expect(posted.out).toContain('POSTED_OK'); + const outage = retry(1); + expect(outage.count).toBe(3); + expect(outage.status).toBe(1); + expect(outage.out).toContain('giving up'); + expect(outage.out.split('retrying').length - 1).toBe(2); }); it('requires the address path to run verification and record it as evidence', () => { From 149b8a1999167b363942f07ab2d8da96bd816ad0 Mon Sep 17 00:00:00 2001 From: verify Date: Thu, 13 Aug 2026 02:09:22 +0800 Subject: [PATCH 5/8] fix(autofix): close the round-3 growth-brake findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Invalidate growth anchors older than the latest stale-base auto-update: the update merges main into the branch and moves the merge base the nets are measured against, so an earlier anchor is no longer comparable — the next round re-anchors at the post-update size instead of misattributing overlap-resolution deltas to review growth. - Pin the merge-base (three-dot) semantics: the measurement fixture now advances main past the divergence, so a two-dot regression changes the expected numbers instead of shipping green. - Pin the sanitize guard's 7-digit cap (9999999 accepted, 10000000 falls back): past it bash integer literals wrap at 64 bits. The census-side hazard (whole-body win= attribution vs multi-key comments) is declined for this PR with the invariant documented at the scanner: the growth marker's key= token cannot match any win= census, and hardening the three censuses to positional attribution is queued as its own change. --- .github/workflows/qwen-autofix.yml | 16 +++++++++++-- scripts/tests/qwen-autofix-workflow.test.js | 25 +++++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index 08c9a740763..4372b3e21ef 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -4100,11 +4100,23 @@ jobs: # autofix-eval marker (a supersede-exempt conflict round reporting # after a re-arm). Any token embedding "win=" would double-attribute # that comment to both windows. - GROWTH_BASELINE="$(jq -r --arg ab "${AUTOFIX_BOT}" --arg key "${LIVE_REARM_KEY}" ' + # A stale-base auto-update merges current main into the branch, + # moving the merge base the nets are measured against: overlap + # resolutions then shift the measurement with no agent push. An + # anchor recorded before the latest base update is not comparable + # any more — ignore it, so the next round re-anchors at the + # post-update size. (A conflict round's own merge of main is the + # narrower residual; its delta is bounded by the overlap.) + BASE_UPD_AT="$(jq -r --arg ab "${AUTOFIX_BOT}" ' + [ .[] | select((.user.login // "") == $ab) + | select((.body // "") | contains("") ] | .[] | {src: (.[0] | tonumber), test: (.[1] | tonumber), win: .[2], at: ($c.created_at // "")} ] - | map(select(.win == $key)) | sort_by(.at) + | map(select(.win == $key)) + | map(select($baseupd == "" or (.at > $baseupd))) | sort_by(.at) | .[0] // empty | "\(.src) \(.test)"' "${WORKDIR}/ic.json")" GROWTH_BASE_NEW='false' if [[ "${GROWTH_BASELINE}" =~ ^(-?[0-9]+)\ (-?[0-9]+)$ ]]; then diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js index ff3c5331255..9bd06430c01 100644 --- a/scripts/tests/qwen-autofix-workflow.test.js +++ b/scripts/tests/qwen-autofix-workflow.test.js @@ -6005,6 +6005,9 @@ exit 1 // …and so do zero-padded values: bash [[ -gt ]] would read '0400' as // octal 256 and raise on '0900', silently disabling the brake. expect(sanitized('0400', '0900')).toBe('400 400'); + // The 7-digit cap is load-bearing: past it bash integer literals wrap + // at 64 bits and comparisons go silently wrong. + expect(sanitized('9999999', '10000000')).toBe('9999999 400'); // Measurement: replay the real block against a real repo. Test lines are // *.test.* / *.spec.* files, __snapshots__/, __tests__/, test-utils/, and @@ -6082,6 +6085,14 @@ exit 1 ); git('add', '-A'); git('commit', '-qm', 'pr'); + // Advance main PAST the divergence before measuring: with main + // unmoved a two-dot regression produces identical numbers, so only a + // moved main pins the merge-base (three-dot) semantics. + git('checkout', '-q', 'main'); + writeFileSync(join(dir, 'mainline.ts'), 'm1\nm2\nm3\nm4\nm5\n'); + git('add', '-A'); + git('commit', '-qm', 'main-moves'); + git('checkout', '-q', 'pr'); git('update-ref', 'refs/remotes/origin/main', 'main'); const measured = execFileSync( 'bash', @@ -6118,7 +6129,7 @@ exit 1 // marker from another login is ignored, negative nets round-trip, and a // window with no marker yields empty (this round anchors it). const baselineJq = prepareBranchAndFeedbackStep.match( - /GROWTH_BASELINE="\$\(jq -r --arg ab "\$\{AUTOFIX_BOT\}" --arg key "\$\{LIVE_REARM_KEY\}" '([\s\S]*?)' "\$\{WORKDIR\}\/ic\.json"\)"/, + /GROWTH_BASELINE="\$\(jq -r --arg ab "\$\{AUTOFIX_BOT\}" --arg key "\$\{LIVE_REARM_KEY\}" --arg baseupd "\$\{BASE_UPD_AT\}" '([\s\S]*?)' "\$\{WORKDIR\}\/ic\.json"\)"/, )?.[1]; expect(baselineJq).toBeTruthy(); const baselineComments = [ @@ -6143,7 +6154,7 @@ exit 1 body: 'report\n\n', }, ]; - const baselineFor = (key) => + const baselineFor = (key, baseupd = '') => execFileSync( 'jq', [ @@ -6154,12 +6165,19 @@ exit 1 '--arg', 'key', key, + '--arg', + 'baseupd', + baseupd, baselineJq, ], { encoding: 'utf8', input: JSON.stringify(baselineComments) }, ).trimEnd(); expect(baselineFor('WIN1')).toBe('50 -60'); expect(baselineFor('WIN2')).toBe(''); + // A base update newer than an anchor invalidates it (the merge base it + // was measured against moved); a later anchor survives first-wins. + expect(baselineFor('WIN1', '2026-01-01T02:30:00Z')).toBe('100 200'); + expect(baselineFor('WIN1', '2026-01-01T04:00:00Z')).toBe(''); // The baseline marker is written into this window's FIRST report only // (pushed and no-op branches), rides the same comment as autofix-eval so @@ -6321,6 +6339,9 @@ exit 1 '--arg', 'key', 'WINX', + '--arg', + 'baseupd', + '', baselineJq, ], { From f0f224cb7ba1758dc2666b554ff91a7e23dad6f4 Mon Sep 17 00:00:00 2001 From: verify Date: Thu, 13 Aug 2026 07:55:32 +0800 Subject: [PATCH 6/8] fix(autofix): skip growth measurement when a managed fork head is named main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare's fork path re-points refs/remotes/origin/main at the fork head for a fork:main PR, so the three-dot measurement would compare the branch against itself and report 0/0 every round — silently disabling the brake while appearing to run. Unmeasurable is unmeasurable: skip and say so, matching the no-merge-base fail-open. --- .github/workflows/qwen-autofix.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index 4372b3e21ef..e238a07d76a 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -4089,8 +4089,19 @@ jobs: # merge base: the three-dot diff exits 128. Fail OPEN to zero like # the merge-tree conflict probe above — an unmeasurable PR skips # the brake rather than dying red at measurement every round. - NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TOTAL=0 - NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TEST=0 + # A managed fork PR whose head branch is literally named 'main' + # makes prepare's fork update-ref re-point refs/remotes/origin/main + # at the fork head — the measurement would compare the branch + # against itself (0/0 forever). Unmeasurable: skip the brake + # (fail open), like the no-merge-base case. + if [[ "${BRANCH}" == 'main' ]]; then + echo "📏 growth measurement skipped: fork head branch is named 'main' (origin/main was re-pointed at checkout)" + NET_TOTAL=0 + NET_TEST=0 + else + NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TOTAL=0 + NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TEST=0 + fi NET_SRC=$(( NET_TOTAL - NET_TEST )) # The marker's window field is spelled `key=`, NOT `win=`, on # purpose: three censuses (PRIOR_TIMEOUTS, the milestone WIN_HEADS, From 45006cb5e29645a58c63692a1a5307a8b0d1c1ab Mon Sep 17 00:00:00 2001 From: verify Date: Thu, 13 Aug 2026 09:47:24 +0800 Subject: [PATCH 7/8] fix(autofix): treat an unmeasurable diff as a state, not zero nets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-substitution anchored a bogus 0/0 baseline on the window's first round (and manufactured phantom growth against an existing anchor). NET_MEASURED now gates the whole brake: no anchor written, no growth computed, no engagement — for both the no-merge-base and fork-head- named-main cases, which are replayed with the skip line and flag asserted. --- .github/workflows/qwen-autofix.yml | 20 ++++++++++++++----- scripts/tests/qwen-autofix-workflow.test.js | 22 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index e238a07d76a..e6edeac34f1 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -4094,13 +4094,19 @@ jobs: # at the fork head — the measurement would compare the branch # against itself (0/0 forever). Unmeasurable: skip the brake # (fail open), like the no-merge-base case. + # Unmeasurable is a STATE, not a zero: substituting 0 nets would + # anchor a bogus 0/0 baseline (or, against an existing anchor, + # manufacture phantom growth). NET_MEASURED gates the whole brake: + # no anchor, no marker, no engagement. + NET_MEASURED='true' + NET_TOTAL=0 + NET_TEST=0 if [[ "${BRANCH}" == 'main' ]]; then echo "📏 growth measurement skipped: fork head branch is named 'main' (origin/main was re-pointed at checkout)" - NET_TOTAL=0 - NET_TEST=0 + NET_MEASURED='false' else - NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TOTAL=0 - NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_TEST=0 + NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_MEASURED='false' + NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_MEASURED='false' fi NET_SRC=$(( NET_TOTAL - NET_TEST )) # The marker's window field is spelled `key=`, NOT `win=`, on @@ -4130,7 +4136,10 @@ jobs: | map(select($baseupd == "" or (.at > $baseupd))) | sort_by(.at) | .[0] // empty | "\(.src) \(.test)"' "${WORKDIR}/ic.json")" GROWTH_BASE_NEW='false' - if [[ "${GROWTH_BASELINE}" =~ ^(-?[0-9]+)\ (-?[0-9]+)$ ]]; then + if [[ "${NET_MEASURED}" != 'true' ]]; then + BASE_SRC=0 + BASE_TEST=0 + elif [[ "${GROWTH_BASELINE}" =~ ^(-?[0-9]+)\ (-?[0-9]+)$ ]]; then BASE_SRC="${BASH_REMATCH[1]}" BASE_TEST="${BASH_REMATCH[2]}" else @@ -4140,6 +4149,7 @@ jobs: fi GROWTH_SRC=$(( NET_SRC - BASE_SRC )) GROWTH_TEST=$(( NET_TEST - BASE_TEST )) + [[ "${NET_MEASURED}" != 'true' ]] && { GROWTH_SRC=0; GROWTH_TEST=0; } { echo "growth_base_new=${GROWTH_BASE_NEW}" echo "growth_base_src=${BASE_SRC}" diff --git a/scripts/tests/qwen-autofix-workflow.test.js b/scripts/tests/qwen-autofix-workflow.test.js index 9bd06430c01..0314b673ea4 100644 --- a/scripts/tests/qwen-autofix-workflow.test.js +++ b/scripts/tests/qwen-autofix-workflow.test.js @@ -6117,6 +6117,26 @@ exit 1 { encoding: 'utf8', cwd: dir }, ); expect(orphan).toBe('0 0 0'); + // Unmeasured is a STATE: no bogus anchor may be written. + const orphanFlag = execFileSync( + 'bash', + [ + '-c', + `set -eo pipefail\n${measureBlock}\nprintf '%s' "$NET_MEASURED"`, + ], + { encoding: 'utf8', cwd: dir }, + ); + expect(orphanFlag).toBe('false'); + // A fork head literally named 'main' skips measurement out loud. + const forkMain = execFileSync( + 'bash', + [ + '-c', + `set -eo pipefail\nBRANCH=main\n${measureBlock}\nprintf '\\n%s %s' "$NET_MEASURED" "$NET_TOTAL"`, + ], + { encoding: 'utf8', cwd: dir }, + ); + expect(forkMain.split('\n').pop()).toBe('false 0'); expect(measureBlock).toContain( "GENERATED_EXCLUDES=(':(exclude,glob)**/package-lock.json' ':(exclude,glob)**/npm-shrinkwrap.json' ':(exclude)packages/vscode-ide-companion/schemas/settings.schema.json')", ); @@ -6301,7 +6321,7 @@ exit 1 'bash', [ '-c', - `GROWTH_BASELINE='${baseline}'\nNET_SRC=${netSrc}\nNET_TEST=${netTest}\n${wiringBlock}\nprintf '%s %s %s %s %s' "$GROWTH_BASE_NEW" "$BASE_SRC" "$BASE_TEST" "$GROWTH_SRC" "$GROWTH_TEST"`, + `GROWTH_BASELINE='${baseline}'\nNET_MEASURED=true\nNET_SRC=${netSrc}\nNET_TEST=${netTest}\n${wiringBlock}\nprintf '%s %s %s %s %s' "$GROWTH_BASE_NEW" "$BASE_SRC" "$BASE_TEST" "$GROWTH_SRC" "$GROWTH_TEST"`, ], { encoding: 'utf8' }, ); From 2d71a0f851c8c18462cc85b60d90973e132274d8 Mon Sep 17 00:00:00 2001 From: verify Date: Thu, 13 Aug 2026 15:37:43 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix(autofix):=20close=20R6=20=E2=80=94=20sh?= =?UTF-8?q?adowed-base=20guard,=20loud=20unmeasured=20skip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A local head branch literally named 'origin/main' shadows the remote ref in rev disambiguation, so the measurement would silently self-compare with NET_MEASURED still true — guard it alongside 'main'. - The unmeasured state now announces itself instead of printing the same 0/0 line as a genuinely empty PR. - SKILL: the batch-budget sentence is scoped to round-threshold engagements, matching the workflow's cause-aware preamble. R6-1 (.gitattributes steering numstat) is declined in-thread: the brake is takeover-quality tooling on the accountability axis — a collaborator with push access holds overt equivalents (removing the label), and a .gitattributes flip is itself a visible diff. --- .github/workflows/qwen-autofix.yml | 8 ++++++-- .qwen/skills/autofix/SKILL.md | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/qwen-autofix.yml b/.github/workflows/qwen-autofix.yml index e6edeac34f1..a327f639145 100644 --- a/.github/workflows/qwen-autofix.yml +++ b/.github/workflows/qwen-autofix.yml @@ -4101,14 +4101,18 @@ jobs: NET_MEASURED='true' NET_TOTAL=0 NET_TEST=0 - if [[ "${BRANCH}" == 'main' ]]; then - echo "📏 growth measurement skipped: fork head branch is named 'main' (origin/main was re-pointed at checkout)" + if [[ "${BRANCH}" == 'main' || "${BRANCH}" == 'origin/main' ]]; then + # 'origin/main' as a LOCAL branch name shadows the remote ref in + # rev disambiguation — the diff would silently self-compare. + echo "📏 growth measurement skipped: head branch name '${BRANCH}' shadows the measurement base" NET_MEASURED='false' else NET_TOTAL="$(git diff --numstat origin/main...HEAD -- "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_MEASURED='false' NET_TEST="$(git diff --numstat origin/main...HEAD -- "${TEST_PATHSPEC[@]}" "${GENERATED_EXCLUDES[@]}" 2> /dev/null | sum_numstat)" || NET_MEASURED='false' fi NET_SRC=$(( NET_TOTAL - NET_TEST )) + [[ "${NET_MEASURED}" != 'true' ]] && + echo "📏 growth measurement UNAVAILABLE this round (no merge base or shadowed base) — brake skipped, no anchor written" # The marker's window field is spelled `key=`, NOT `win=`, on # purpose: three censuses (PRIOR_TIMEOUTS, the milestone WIN_HEADS, # the breaker PRIOR_HEADS) attribute comments to windows by the diff --git a/.qwen/skills/autofix/SKILL.md b/.qwen/skills/autofix/SKILL.md index 32056bdddf5..40052293cb4 100644 --- a/.qwen/skills/autofix/SKILL.md +++ b/.qwen/skills/autofix/SKILL.md @@ -312,8 +312,9 @@ implement — satisfying a nit is never a reason to bloat the code. not work: do not modify code, resolve threads, or write comment replies for those items. Everything rendered in the actionable sections IS in scope — the deterministic filter defers the automated reviewer's non-Critical - suggestions and, past a small per-window budget of already-addressed - batches, a human author's untagged feedback too (an account can host an + suggestions and, once the ROUND threshold has engaged (never during a + growth-only engagement), past a small per-window budget of + already-addressed batches, a human author's untagged feedback too (an account can host an automated reviewer loop, so the brake keys on measured regeneration, not identity). A maintainer writing "fix X before merge" after round five means exactly that when it reaches you — plus failed checks and the