mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-27 01:23:52 +00:00
feat(autofix): audit the approach instead of stopping on growth-budget breach (#9262)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(autofix): audit the approach instead of stopping on growth-budget breach A growth-budget breach no longer escalates to a maintainer handoff that stops the takeover. The breach now makes the round a growth-audit round: the agent audits the PR's approach on two axes — KISS (name a simpler alternative or prove each piece load-bearing) and minimal change (every hunk traces to the problem, an accepted finding, or a failing check) — and records a machine-readable verdict that the verification gate requires. sound re-arms the counting window at the current size and the loop keeps solving; drift simplifies first, then continues; conflict is the only growth path to a human, parked idempotently until a trusted human responds. The old divergence ladder (over budget for N rounds and not shrinking → stop) terminated takeovers whose remaining work could still fit: the growth it punished was protocol-mandated pinned tests (#9213 stalled at round 5 with two small Criticals left). A size signal now triggers a judgment, never a stop. Design: docs/design/autofix-growth-audit.md * fix(autofix): update the artifact-list pin for the growth-audit.json upload entry * fix(autofix): surface conflict verdicts past the failure.md exits and strip verdict forgery channels (#9262) * fix(autofix): harden the growth-audit verdict pipeline and park wake set (#9262) * fix(autofix): close the verdict-pipeline forgeries and loop-generated wake entrances (#9262) * fix(ci): drop the retired divergence rationale records (af-046/af-047) from qwen-autofix.md --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
This commit is contained in:
parent
e443f26bfc
commit
2c64ebe980
7 changed files with 2825 additions and 518 deletions
361
.github/scripts/run-autofix-review-verification.sh
vendored
361
.github/scripts/run-autofix-review-verification.sh
vendored
|
|
@ -45,6 +45,42 @@ git config --file "${GIT_CONFIG_GLOBAL}" safe.directory "$(pwd)"
|
|||
if [ -s /etc/gitconfig ]; then
|
||||
echo "::notice::/etc/gitconfig exists but is bypassed by the gate's GIT_CONFIG_SYSTEM redirect — replicate any setting the checks need via per-job env."
|
||||
fi
|
||||
# Two more inherited knobs steer EXECUTION itself, and neither has a
|
||||
# legitimate setter: BASH_ENV names a file every non-interactive bash
|
||||
# sources at STARTUP — a body-side unset is one hop late (bash sources a
|
||||
# plant before line 1), so the verify steps pin it empty at step level AND
|
||||
# launch this gate through their env -i clean child; the unset here keeps
|
||||
# the gate's own bash children clean too. BITE_RUNNER selects the bite
|
||||
# check's runner command, which executes unwrapped with the gate's full
|
||||
# environment. Strip them with the GIT_* class.
|
||||
unset BASH_ENV BITE_RUNNER
|
||||
# The verdict variables are GATE state, not inherited state: a plant of
|
||||
# AUDIT_VERDICT_RECORDED=true plus a verdict from an earlier step would
|
||||
# otherwise ride the every-exit re-append back into this step's outputs on
|
||||
# paths where the gate validated nothing.
|
||||
unset AUDIT_VERDICT AUDIT_VERDICT_RECORDED
|
||||
# The runner backs $GITHUB_ENV/$GITHUB_PATH/$GITHUB_STEP_SUMMARY with files
|
||||
# under $RUNNER_TEMP/_runner_file_commands/ that it reads back at step end.
|
||||
# The channel strip below removes the VARIABLES from the checks, but the
|
||||
# files stay discoverable under the inherited (predictable) $RUNNER_TEMP
|
||||
# and stay WRITABLE — a check that appends there plants environment into
|
||||
# every later step of this job, the PAT-bearing one included (discovery
|
||||
# verified on a live runner). Lock the files for the lifetime of this
|
||||
# step. The $GITHUB_OUTPUT backing file is the ONE exception: the gate
|
||||
# must keep writing it, and forges against it lose to the every-exit
|
||||
# re-append below plus the conclusion gate Finalize verification applies
|
||||
# to outcome. The directory itself stays writable on purpose: the runner
|
||||
# creates the NEXT step's backing files there at step start, and a locked
|
||||
# directory would stall every later step of the job; the residual
|
||||
# rename-over (create + rename onto a locked file) is documented in the
|
||||
# design doc instead of bought at that price.
|
||||
if [[ -n "${GITHUB_OUTPUT:-}" && -d "${RUNNER_TEMP}/_runner_file_commands" ]]; then
|
||||
for _rfc in "${RUNNER_TEMP}/_runner_file_commands"/*; do
|
||||
if [[ -f "${_rfc}" && "${_rfc}" != "${GITHUB_OUTPUT}" ]]; then
|
||||
chmod a-w "${_rfc}" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Record whether the agent left a commit FIRST — this is a ref-only
|
||||
# diff, so it runs before the failure.md early-exits and covers an
|
||||
|
|
@ -60,83 +96,6 @@ if [[ "${committed_rc}" -eq 1 ]]; then
|
|||
echo "committed=true" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
if [[ -f "${WORKDIR}/failure.md" && -n "$(git status --porcelain)" ]]; then
|
||||
echo "❌ Agent wrote failure.md after leaving a dirty workspace:"
|
||||
git status --short
|
||||
cat "${WORKDIR}/failure.md"
|
||||
echo "outcome=failed" >> "${GITHUB_OUTPUT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "${WORKDIR}/failure.md" ]]; then
|
||||
echo "🛑 Agent aborted intentionally:"
|
||||
cat "${WORKDIR}/failure.md"
|
||||
echo "outcome=failed" >> "${GITHUB_OUTPUT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A handoff claims the round changed NOTHING — dirt beside it is a
|
||||
# brake-violating partial patch (otherwise reported as a clean stop and
|
||||
# discarded silently with the runner), and untracked leftovers would trip
|
||||
# the NEXT round's dirty assert on the persistent pool. The ref-level
|
||||
# commit diff below is blind to both. Non-retryable like failure.md+dirty
|
||||
# above (a retryable rejection would engage the repair pass, which deletes
|
||||
# handoff.md and may commit against the brake), but under its OWN outcome:
|
||||
# outcome=failed would make the report step dress the rejection as a
|
||||
# failed FIX ("could not produce a passing fix", or a stale-base retry
|
||||
# promise) when no fix existed — the report step gives this shape its own
|
||||
# honest headline.
|
||||
if [[ -s "${WORKDIR}/handoff.md" && -n "$(git status --porcelain)" ]]; then
|
||||
echo "❌ Agent wrote handoff.md after leaving a dirty workspace:"
|
||||
git status --short
|
||||
sed 's/::/;;/g' "${WORKDIR}/handoff.md"
|
||||
echo "outcome=dirty_handoff" >> "${GITHUB_OUTPUT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The committed sibling of the brake violation above: the round HAS a commit
|
||||
# beside handoff.md. Judged by dirt alone it slips both guards — the dirty
|
||||
# check sees a clean tree, and the no-commit handoff branch below requires
|
||||
# an unchanged ref — so it would reach the structural checks, where
|
||||
# reject_fix defaults to retryable and the repair pass deletes
|
||||
# handoff.md and may commit AGAIN against the brake's stop. Non-retryable
|
||||
# under its OWN outcome: a commit DID happen, so the dirty-handoff headline
|
||||
# claiming nothing was committed would misreport it. Same reasoning as the
|
||||
# dirty guard otherwise.
|
||||
if [[ -s "${WORKDIR}/handoff.md" && "${committed_rc:-0}" -eq 1 ]]; then
|
||||
echo "❌ Agent wrote handoff.md but the round HAS a commit — a brake violation:"
|
||||
git log --oneline "origin/${BRANCH}..${BRANCH}"
|
||||
sed 's/::/;;/g' "${WORKDIR}/handoff.md"
|
||||
echo "outcome=committed_handoff" >> "${GITHUB_OUTPUT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# No-commit brake handoff, classified BEFORE the structural checks below:
|
||||
# those judge the PR's OWN diff (core rebuild, schema freshness, contracts)
|
||||
# and reject_fix on failure, and the growth brake fires on exactly the red
|
||||
# PRs whose diff trips them. A compliant handoff commits nothing, so
|
||||
# running the checks first would reclassify it as a retryable failure —
|
||||
# the repair pass would delete handoff.md and commit against the brake's
|
||||
# stop. A handoff claims nothing (acted=false, deferred to a human), so
|
||||
# the checks' false-no-action rationale does not apply. failure.md
|
||||
# coexistence keeps the failed classification via the exits above.
|
||||
if git diff --quiet "origin/${BRANCH}...${BRANCH}" \
|
||||
&& [[ -s "${WORKDIR}/handoff.md" ]]; then
|
||||
echo "🤝 Branch unchanged with a handoff — the agent stopped under instruction and deferred this item to a human:"
|
||||
# Agent-written content: a line-start `::` would be parsed as a workflow
|
||||
# command (::error::, ::add-mask::), the same reason 'Show run artifacts'
|
||||
# neutralizes these files.
|
||||
sed 's/::/;;/g' "${WORKDIR}/handoff.md"
|
||||
echo "outcome=handoff" >> "${GITHUB_OUTPUT}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convention: hooks are severed at EVERY host checkout of the PR
|
||||
# branch (no secret sits in this step's env, but a post-checkout
|
||||
# hook still runs branch code on the host).
|
||||
git config core.hooksPath /dev/null
|
||||
git checkout "${BRANCH}"
|
||||
|
||||
GATE_LOG="${WORKDIR}/gate-output.log"
|
||||
: > "${GATE_LOG}"
|
||||
rm -f "${GATE_LOG}.bite"
|
||||
|
|
@ -153,6 +112,10 @@ reject_fix() {
|
|||
# step means the gate itself crashed, so losing the detail file must not turn
|
||||
# a deterministic rejection into an infrastructure retry.
|
||||
echo "outcome=failed" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
if [[ "${preexisting}" == 'true' ]]; then
|
||||
# NOT retryable: the repair agent is only allowed to amend this round's
|
||||
# fix, and a failure that exists without the fix is outside that boundary
|
||||
|
|
@ -185,6 +148,196 @@ reject_fix() {
|
|||
echo "::warning::could not write the gate rejection detail; the verdict stands."
|
||||
exit 1
|
||||
}
|
||||
# Last-writer binding for the audit verdict: the record below happens
|
||||
# BEFORE the branch's build/tests run, and a check can still discover the
|
||||
# step-output FILE through the inherited $RUNNER_TEMP (the strip removes
|
||||
# the variable, not the backing file) and append its own audit_verdict —
|
||||
# step outputs are last-write-wins. EVERY exit therefore re-appends the
|
||||
# validated verdict INLINE (no function call: gate snippets extracted by
|
||||
# the contract suite must stay executable standalone), including the exits
|
||||
# that run after branch checks (a forge appended mid-check loses to the
|
||||
# exit's rewrite) — so the gate's copy outwrites any forged append. The
|
||||
# flag gates it: a verdict rejected BEFORE its record (missing, malformed,
|
||||
# or a routing violation) never surfaces. kiss_audit rides the same
|
||||
# discipline (recorded above, re-appended unconditionally at every exit).
|
||||
# Defended control-bit surface: kiss_audit reaches every later step ONLY
|
||||
# through this output — recorded HERE, before any branch code runs in this
|
||||
# step, and re-appended at every exit below with the same last-writer
|
||||
# discipline as the verdict. A consumer that read steps.prepare's copy
|
||||
# directly would route the bit around the gate's defenses.
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# Growth-audit verdict gate: a round tagged KISS_AUDIT (its counting window
|
||||
# is over the growth budget) must carry the audit's machine-readable verdict
|
||||
# — the audit IS the round's judgment of the over-budget approach, and a
|
||||
# round that skipped it must not push (the rubber-stamp hole by absence).
|
||||
# Sits BEFORE the failure.md early-exits below: a conflict round stops
|
||||
# BLOCKED via failure.md, and its verdict must be validated and surfaced to
|
||||
# GITHUB_OUTPUT before that exit writes outcome=failed — otherwise the
|
||||
# conflict trail marker never posts and the idempotent park never engages.
|
||||
# Also before the build/schema/footprint checks AND the no-commit/no-op
|
||||
# exits further down: the verdict is required even for a no-op audit round
|
||||
# whose verdict is sound with nothing left to fix. Malformed is agent
|
||||
# misbehavior, not a build problem — NON-retryable, so the repair pass is
|
||||
# never invoked and the next scan simply re-runs the audit.
|
||||
if [[ "${KISS_AUDIT:-false}" == 'true' ]]; then
|
||||
AUDIT_VERDICT=''
|
||||
if [[ -f "${WORKDIR}/growth-audit.json" ]]; then
|
||||
# Slurp so the document COUNT is part of validation: the per-document
|
||||
# parse accepted a valid first document followed by one jq errors on
|
||||
# (or shape-filters out) on the FIRST document's verdict — the gate's
|
||||
# contract is a single JSON document, so reject every multi-document
|
||||
# stream.
|
||||
AUDIT_VERDICT="$(jq -rs '
|
||||
if length != 1 then empty else .[0]
|
||||
| select((.verdict // "") | IN("sound", "drift", "conflict"))
|
||||
| select((.kiss.result // "") | IN("pass", "fail"))
|
||||
| select((.minimal_change.result // "") | IN("pass", "fail"))
|
||||
| select((.verdict != "sound")
|
||||
or ((.kiss.result == "pass") and (.minimal_change.result == "pass")))
|
||||
| select((.verdict != "drift")
|
||||
or ((.kiss.result == "fail") or (.minimal_change.result == "fail")))
|
||||
| .verdict end' "${WORKDIR}/growth-audit.json" 2> /dev/null || true)"
|
||||
fi
|
||||
# Anchor the parsed value (defense in depth now that slurp rejects
|
||||
# multi-document streams outright).
|
||||
[[ "${AUDIT_VERDICT}" =~ ^(sound|drift|conflict)$ ]] || AUDIT_VERDICT=''
|
||||
if [[ -z "${AUDIT_VERDICT}" ]]; then
|
||||
{
|
||||
echo "Growth-audit round (this counting window is over its growth budget) without a valid growth-audit.json verdict."
|
||||
echo "The audit must run BEFORE any edit this round, and the verdict file must be a single JSON document carrying verdict sound|drift|conflict plus kiss.result and minimal_change.result each pass|fail, consistent with the taxonomy (sound requires both axes pass; drift requires at least one axis fail). Re-run the audit and produce the file; do not push without it."
|
||||
} >> "${GATE_LOG}"
|
||||
reject_fix 'growth-audit round missing a valid growth-audit.json verdict (audit skipped or malformed)' 'false' 'false'
|
||||
fi
|
||||
echo "🔎 growth-audit verdict: ${AUDIT_VERDICT}"
|
||||
# Conflict routing is enforced HERE, not by convention: a conflict verdict
|
||||
# must STOP BLOCKED with a handoff (the only growth path to a human). A
|
||||
# round that kept fixing and committed would otherwise clear the gate like
|
||||
# sound/drift, push the contested code, and park the next scan on a
|
||||
# handoff question that was never asked. NON-retryable: re-audit, don't
|
||||
# repair.
|
||||
if [[ "${AUDIT_VERDICT}" == 'conflict' && ! -f "${WORKDIR}/failure.md" && ! -s "${WORKDIR}/handoff.md" ]]; then
|
||||
reject_fix 'growth-audit verdict is conflict but the round did not stop with a handoff; conflict must STOP BLOCKED (no push)' 'false' 'false'
|
||||
fi
|
||||
# The no-push half of this routing is enforced at the success exit
|
||||
# below: a repair pass re-auditing to conflict LEGITIMATELY runs behind
|
||||
# the first pass's commit (committed_rc=1), so the push shape cannot be
|
||||
# refused here without refusing it — the refusal sits at the push
|
||||
# boundary itself.
|
||||
# Record the verdict the GATE validated, for the report step to consume
|
||||
# via the step output. The report must NOT re-read the file itself: the
|
||||
# branch's own build/tests run as the runner user after this point and
|
||||
# WORKDIR is a predictable path they can write — the validated verdict is
|
||||
# the only verdict that may reach the trail marker and the re-arm.
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
AUDIT_VERDICT_RECORDED='true'
|
||||
fi
|
||||
|
||||
if [[ -f "${WORKDIR}/failure.md" && -n "$(git status --porcelain)" ]]; then
|
||||
echo "❌ Agent wrote failure.md after leaving a dirty workspace:"
|
||||
git status --short
|
||||
cat "${WORKDIR}/failure.md"
|
||||
echo "outcome=failed" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "${WORKDIR}/failure.md" ]]; then
|
||||
echo "🛑 Agent aborted intentionally:"
|
||||
cat "${WORKDIR}/failure.md"
|
||||
echo "outcome=failed" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# These three handoff classifications skip a growth-audit CONFLICT verdict:
|
||||
# that round has its own routing — the verdict gate (stop enforced here) and
|
||||
# the push-boundary refusal at the success exit — and it must land
|
||||
# outcome=failed so the conflict trail marker posts and the park engages,
|
||||
# never the clean outcome=handoff. A plain (non-audit) handoff still takes
|
||||
# these.
|
||||
# A handoff claims the round changed NOTHING — dirt beside it is a
|
||||
# brake-violating partial patch (otherwise reported as a clean stop and
|
||||
# discarded silently with the runner), and untracked leftovers would trip
|
||||
# the NEXT round's dirty assert on the persistent pool. The ref-level
|
||||
# commit diff below is blind to both. Non-retryable like failure.md+dirty
|
||||
# above (a retryable rejection would engage the repair pass, which deletes
|
||||
# handoff.md and may commit against the brake), but under its OWN outcome:
|
||||
# outcome=failed would make the report step dress the rejection as a
|
||||
# failed FIX ("could not produce a passing fix", or a stale-base retry
|
||||
# promise) when no fix existed — the report step gives this shape its own
|
||||
# honest headline.
|
||||
if [[ -s "${WORKDIR}/handoff.md" && -n "$(git status --porcelain)" \
|
||||
&& "${AUDIT_VERDICT:-}" != 'conflict' ]]; then
|
||||
echo "❌ Agent wrote handoff.md after leaving a dirty workspace:"
|
||||
git status --short
|
||||
sed 's/::/;;/g' "${WORKDIR}/handoff.md"
|
||||
echo "outcome=dirty_handoff" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The committed sibling of the brake violation above: the round HAS a commit
|
||||
# beside handoff.md. Judged by dirt alone it slips both guards — the dirty
|
||||
# check sees a clean tree, and the no-commit handoff branch below requires
|
||||
# an unchanged ref — so it would reach the structural checks, where
|
||||
# reject_fix defaults to retryable and the repair pass deletes
|
||||
# handoff.md and may commit AGAIN against the brake's stop. Non-retryable
|
||||
# under its OWN outcome: a commit DID happen, so the dirty-handoff headline
|
||||
# claiming nothing was committed would misreport it. Same reasoning as the
|
||||
# dirty guard otherwise.
|
||||
if [[ -s "${WORKDIR}/handoff.md" && "${committed_rc:-0}" -eq 1 \
|
||||
&& "${AUDIT_VERDICT:-}" != 'conflict' ]]; then
|
||||
echo "❌ Agent wrote handoff.md but the round HAS a commit — a brake violation:"
|
||||
git log --oneline "origin/${BRANCH}..${BRANCH}"
|
||||
sed 's/::/;;/g' "${WORKDIR}/handoff.md"
|
||||
echo "outcome=committed_handoff" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# No-commit brake handoff, classified BEFORE the structural checks below:
|
||||
# those judge the PR's OWN diff (core rebuild, schema freshness, contracts)
|
||||
# and reject_fix on failure, and the growth brake fires on exactly the red
|
||||
# PRs whose diff trips them. A compliant handoff commits nothing, so
|
||||
# running the checks first would reclassify it as a retryable failure —
|
||||
# the repair pass would delete handoff.md and commit against the brake's
|
||||
# stop. A handoff claims nothing (acted=false, deferred to a human), so
|
||||
# the checks' false-no-action rationale does not apply. failure.md
|
||||
# coexistence keeps the failed classification via the exits above.
|
||||
if git diff --quiet "origin/${BRANCH}...${BRANCH}" \
|
||||
&& [[ -s "${WORKDIR}/handoff.md" ]] \
|
||||
&& [[ "${AUDIT_VERDICT:-}" != 'conflict' ]]; then
|
||||
echo "🤝 Branch unchanged with a handoff — the agent stopped under instruction and deferred this item to a human:"
|
||||
# Agent-written content: a line-start `::` would be parsed as a workflow
|
||||
# command (::error::, ::add-mask::), the same reason 'Show run artifacts'
|
||||
# neutralizes these files.
|
||||
sed 's/::/;;/g' "${WORKDIR}/handoff.md"
|
||||
echo "outcome=handoff" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convention: hooks are severed at EVERY host checkout of the PR
|
||||
# branch (no secret sits in this step's env, but a post-checkout
|
||||
# hook still runs branch code on the host).
|
||||
git config core.hooksPath /dev/null
|
||||
git checkout "${BRANCH}"
|
||||
baseline_also_fails() {
|
||||
# A deterministic rejection is only chargeable to this round if the same
|
||||
# check passes WITHOUT the round's commits. Measured counterexample, run
|
||||
|
|
@ -232,7 +385,7 @@ baseline_also_fails() {
|
|||
local ab_log="${GATE_LOG}.baseline"
|
||||
: > "${ab_log}"
|
||||
rc=0
|
||||
if ! "$@" >> "${ab_log}" 2>&1; then
|
||||
if ! strip_runner_channels "$@" >> "${ab_log}" 2>&1; then
|
||||
rc=1
|
||||
fi
|
||||
git restore -- . 2>> "${GATE_LOG}" || true
|
||||
|
|
@ -253,6 +406,10 @@ baseline_also_fails() {
|
|||
tail -c 3000 "${GATE_LOG}" 2> /dev/null
|
||||
echo '````'
|
||||
} > "${WORKDIR}/gate-rejection.md" || true
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
# Every retryable exit below hands the tree to the repair agent with
|
||||
|
|
@ -327,6 +484,20 @@ fail_signature() {
|
|||
seed_dist_note() {
|
||||
echo "⚠️ the baseline leg rebuilt dist/ from baseline sources — run npm run build before typecheck/tests" >> "${GATE_LOG}"
|
||||
}
|
||||
# Every check below runs the BRANCH's own code (npm scripts, tests, and
|
||||
# their lifecycle children) with this step's inherited environment. Strip
|
||||
# the runner injection channels first: a check appending to GITHUB_OUTPUT
|
||||
# would overwrite the gate's own outputs last-write-wins (a forged
|
||||
# audit_verdict=sound after the gate's write), GITHUB_ENV/GITHUB_PATH
|
||||
# plant environment for the PAT-bearing steps that follow, and
|
||||
# GITHUB_STEP_SUMMARY lets branch code forge the job summary styled as
|
||||
# gate output (the display-channel sibling; qwen-triage strips it when
|
||||
# running external-author branch code for the same reason). Same class the
|
||||
# deferred-upsert child closes with env -i; targeted -u here because the
|
||||
# checks need the ordinary environment (PATH, HOME, …) to run at all.
|
||||
strip_runner_channels() {
|
||||
env -u GITHUB_OUTPUT -u GITHUB_ENV -u GITHUB_PATH -u GITHUB_STEP_SUMMARY "$@"
|
||||
}
|
||||
run_check() {
|
||||
# pipefail makes the pipeline carry the command's status, not tee's. The
|
||||
# side copy holds THIS check's transcript alone — the identity comparison
|
||||
|
|
@ -334,7 +505,7 @@ run_check() {
|
|||
local label="${1}"
|
||||
shift
|
||||
: > "${GATE_LOG}.check"
|
||||
if ! "$@" 2>&1 | tee -a "${GATE_LOG}" "${GATE_LOG}.check"; then
|
||||
if ! strip_runner_channels "$@" 2>&1 | tee -a "${GATE_LOG}" "${GATE_LOG}.check"; then
|
||||
if baseline_also_fails "$@"; then
|
||||
reject_fix "${label} (pre-existing: also fails without this round's commit)" 'true'
|
||||
fi
|
||||
|
|
@ -354,7 +525,7 @@ run_check_no_ab() {
|
|||
# allowlist).
|
||||
local label="${1}"
|
||||
shift
|
||||
if ! "$@" 2>&1 | tee -a "${GATE_LOG}"; then
|
||||
if ! strip_runner_channels "$@" 2>&1 | tee -a "${GATE_LOG}"; then
|
||||
reject_fix "${label}"
|
||||
fi
|
||||
}
|
||||
|
|
@ -418,16 +589,28 @@ if git diff --quiet "origin/${BRANCH}...${BRANCH}"; then
|
|||
cat "${WORKDIR}/no-action.md"
|
||||
echo "verified_head=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
|
||||
echo "outcome=noop" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
echo "❌ Branch unchanged and no no-action.md — agent produced nothing"
|
||||
echo "outcome=failed" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -s "${WORKDIR}/address-summary.md" ]]; then
|
||||
echo "❌ Branch changed but address-summary.md is missing"
|
||||
echo "outcome=failed" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -897,7 +1080,7 @@ bite_runner_default() {
|
|||
# $1 = workspace dir, rest = test paths relative to the workspace.
|
||||
local ws="${1}"
|
||||
shift
|
||||
npm run test --workspace "${ws}" --if-present -- "$@"
|
||||
strip_runner_channels npm run test --workspace "${ws}" --if-present -- "$@"
|
||||
}
|
||||
mapfile -d '' -t BITE_FILES < <(git diff --name-only -z --no-renames --diff-filter=AM "${ROUND_RANGE}" \
|
||||
-- ':(glob)**/*.test.*' ':(glob)**/*.spec.*' ':(exclude,glob)**/__snapshots__/**' \
|
||||
|
|
@ -1066,6 +1249,10 @@ if [[ "${#BITE_FILES[@]}" -gt 0 && -n "${BITE_SRC}" ]]; then
|
|||
tail -c 3000 "${GATE_LOG}" 2> /dev/null
|
||||
echo '````'
|
||||
} > "${WORKDIR}/gate-rejection.md" || true
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
git reset --quiet 2>> "${GATE_LOG}" || true
|
||||
|
|
@ -1116,5 +1303,17 @@ if [[ "${#BITE_FILES[@]}" -gt 0 && -n "${BITE_SRC}" ]]; then
|
|||
fi
|
||||
fi
|
||||
assert_verification_tree
|
||||
# A conflict verdict must STOP BLOCKED: completing as fixed would push the
|
||||
# contested code under the PAT while the report posts the park marker —
|
||||
# the exact outcome the routing check above exists to prevent. The routing
|
||||
# check cannot see this shape (a planted handoff.md satisfies it), so
|
||||
# refuse at the push boundary. NON-retryable: re-audit, don't repair.
|
||||
if [[ "${AUDIT_VERDICT:-}" == 'conflict' ]]; then
|
||||
reject_fix 'growth-audit verdict is conflict but the round completed as fixed; conflict must STOP BLOCKED (no push)' 'false' 'false'
|
||||
fi
|
||||
echo "verified_head=${VERIFICATION_HEAD}" >> "${GITHUB_OUTPUT}"
|
||||
echo "outcome=fixed" >> "${GITHUB_OUTPUT}"
|
||||
echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}"
|
||||
if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then
|
||||
echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue