Repository navigation
feat(autofix): audit the approach instead of stopping on growth-budget breach #9262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
a747f64
364d28d
f7c3c66
ba4eb09
b5e8ca6
7bb4d2f
cfde2d9
4aaee34
5430b6e
df5a79e
361c745
a4c984e
4653f8c
2c73f3d
33168c6
9ae9e7c
c2ebf0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,15 @@ 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 | ||
| # child sources at startup — a $GITHUB_ENV plant from an earlier step | ||
| # would run inside the trusted helpers this gate spawns, past the channel | ||
| # strip below (it covers their environment, not bash's startup file) — | ||
| # and 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 | ||
|
|
||
| # 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,27 +69,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 | ||
|
|
||
| # 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" | ||
|
|
@@ -97,6 +85,9 @@ 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}" | ||
| 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 | ||
|
|
@@ -129,6 +120,102 @@ 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 — | ||
|
Comment on lines
+151
to
+154
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred per the maintainer's split decision (option A, 2026-08-18): this finding is tracked as a follow-up in #9374 rather than fixed in this PR, and the live escalation thread carries the open decision. Leaving this open until it lands there.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escalated for a maintainer decision — no code change to this surface this round. The finding is verified against the code at this commit (7bb4d2f):
Why escalation and not a patch: four rounds of entrance-by-entrance hardening on this surface have each found new entrances in the prior round's hardening — round 3's seal itself is the broken piece this round. The finding's conclusion that the surface is unbounded under per-entrance patching matches that trajectory, and the Critical-only growth brake is engaged (window diff net src 423 / test 880 vs 400/400 budgets). The fix directions carry different scope/risk trades that are the maintainer's call:
Recommendation: A — the machinery handles the non-adversarial churn it was built for; the trust-domain question deserves its own design discussion rather than a round-5 patch on an over-budget diff. Question for the maintainer: which direction — split (A), redesign in place (B), or accept with repriced documented residuals (C)? 中文说明升级为 maintainer 决策——本轮不对该表面做任何代码修改。 该发现已在当前 commit(7bb4d2fde)的代码上核实:
为什么升级而不是打补丁:该表面经过四轮逐入口加固,每一轮都在上一轮的加固上发现新入口——本轮被打破的正是 round 3 的 seal。「该表面无法逐个入口封堵」的发现结论与这一轨迹吻合,且 Critical-only 增长刹车已触发(窗口净增长 src 423 / test 880,预算 400/400)。各修复方向带有不同的范围/风险权衡,属于 maintainer 决策:
建议:A——现有机制能处理其设计目标内的非对抗性 churn;信任域问题值得一场独立的设计讨论,而不是在已超预算的 diff 上做第 5 轮补丁。 向 maintainer 提问:选择哪个方向——拆分(A)、原地重设计(B)、还是接受并重估文档化残留(C)? |
||
| # step outputs are last-write-wins. Every exit past the record therefore | ||
| # re-appends the validated verdict INLINE (no function call: gate snippets | ||
| # extracted by the contract suite must stay executable standalone), 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. | ||
| # 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='' | ||
|
wenshao marked this conversation as resolved.
Resolved
|
||
| if [[ -f "${WORKDIR}/growth-audit.json" ]]; then | ||
|
Comment on lines
+184
to
+185
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred per the maintainer's split decision (option A, 2026-08-18): this finding is tracked as a follow-up in #9374 rather than fixed in this PR, and the live escalation thread carries the open decision. Leaving this open until it lands there. |
||
| # 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred to the next round — not declined. Critical-only mode is active (the window's diff is over the growth budget), so this round's batch was limited to the two standing Criticals — R1-2 (verdict-pipeline forgery entrances) and R2-6 (loop-generated park wakes) — which this commit closes. This suggestion stays open and is picked up next round. The conflict routing condition still mixes -f (failure.md) and -s (handoff.md) existence tests. 中文说明延迟到下一轮处理——并非拒绝。Critical-only 模式已生效(窗口 diff 已超增长预算),因此本轮批次仅限于两个仍然成立的 Critical——R1-2(verdict 流水线伪造入口)与 R2-6(循环自产的 park 唤醒)——本次提交已将它们关闭。本建议保持开放,下一轮处理。 conflict 路由条件仍混用 -f(failure.md)与 -s(handoff.md)存在性判断。 |
||
| 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}" | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred per the maintainer's split decision (option A, 2026-08-18): this finding is tracked as a follow-up in #9374 rather than fixed in this PR, and the live escalation thread carries the open decision. Leaving this open until it lands there. |
||
| 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}" | ||
| 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 | ||
|
|
||
| # 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 | ||
|
|
@@ -176,7 +263,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 | ||
|
|
@@ -197,6 +284,9 @@ baseline_also_fails() { | |
| tail -c 3000 "${GATE_LOG}" 2> /dev/null | ||
| echo '````' | ||
| } > "${WORKDIR}/gate-rejection.md" || true | ||
| 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 | ||
|
|
@@ -271,14 +361,28 @@ 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 | ||
| # must not match diagnostics an earlier check left in the shared log. | ||
| 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 | ||
|
|
@@ -298,7 +402,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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred to the next round (batch bound). The gap is real — the 因批次上限推迟到下一轮。该缺口成立——
Comment on lines
526
to
+528
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred to the next round — not declined. Critical-only mode is active (the window's diff is over the growth budget), so this round's batch was limited to the two standing Criticals — R1-2 (verdict-pipeline forgery entrances) and R2-6 (loop-generated park wakes) — which this commit closes. This suggestion stays open and is picked up next round. The strip wrap inside run_check_no_ab still has zero behavioral coverage (the forge stub only fires on npm run build). 中文说明延迟到下一轮处理——并非拒绝。Critical-only 模式已生效(窗口 diff 已超增长预算),因此本轮批次仅限于两个仍然成立的 Critical——R1-2(verdict 流水线伪造入口)与 R2-6(循环自产的 park 唤醒)——本次提交已将它们关闭。本建议保持开放,下一轮处理。 run_check_no_ab 内部的 strip 包装仍然没有任何行为覆盖(伪造 stub 只在 npm run build 时触发)。 |
||
| reject_fix "${label}" | ||
| fi | ||
| } | ||
|
|
@@ -360,6 +464,9 @@ 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}" | ||
| if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then | ||
| echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}" | ||
|
Comment on lines
591
to
+594
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred to the next round — not declined. Critical-only mode is active (the window's diff is over the growth budget), so this round's batch was limited to the two standing Criticals — R1-2 (verdict-pipeline forgery entrances) and R2-6 (loop-generated park wakes) — which this commit closes. This suggestion stays open and is picked up next round. Routing still accepts a conflict round on a non-empty handoff.md alone, and that shape can exit via the noop path without re-evaluating the handoff question. 中文说明延迟到下一轮处理——并非拒绝。Critical-only 模式已生效(窗口 diff 已超增长预算),因此本轮批次仅限于两个仍然成立的 Critical——R1-2(verdict 流水线伪造入口)与 R2-6(循环自产的 park 唤醒)——本次提交已将它们关闭。本建议保持开放,下一轮处理。 路由仍接受仅凭非空 handoff.md 的 conflict 轮,且该形态可以走 noop 出口而不重新评估 handoff 问题。 |
||
| fi | ||
| exit 0 | ||
| fi | ||
| echo "❌ Branch unchanged and no no-action.md — agent produced nothing" | ||
|
|
@@ -839,7 +946,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__/**' \ | ||
|
|
@@ -1008,6 +1115,9 @@ if [[ "${#BITE_FILES[@]}" -gt 0 && -n "${BITE_SRC}" ]]; then | |
| tail -c 3000 "${GATE_LOG}" 2> /dev/null | ||
| echo '````' | ||
| } > "${WORKDIR}/gate-rejection.md" || true | ||
| if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then | ||
| echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}" | ||
| fi | ||
| exit 1 | ||
| } | ||
| git reset --quiet 2>> "${GATE_LOG}" || true | ||
|
|
@@ -1058,5 +1168,16 @@ 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}" | ||
| if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then | ||
| echo "audit_verdict=${AUDIT_VERDICT}" >> "${GITHUB_OUTPUT}" | ||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deferred to the next round — not declined. Critical-only mode is active (the window's diff is over the growth budget), so this round's batch was limited to the two standing Criticals — R1-2 (verdict-pipeline forgery entrances) and R2-6 (loop-generated park wakes) — which this commit closes. This suggestion stays open and is picked up next round.
Note: this round replaced the startup-sourcing channel itself (step-level BASH_ENV/SHELLOPTS pins + the env -i clean child, pinned in the tests); the suggested behavioral probe that PLANTS a BASH_ENV against the wrapper remains open for next round.
中文说明
延迟到下一轮处理——并非拒绝。Critical-only 模式已生效(窗口 diff 已超增长预算),因此本轮批次仅限于两个仍然成立的 Critical——R1-2(verdict 流水线伪造入口)与 R2-6(循环自产的 park 唤醒)——本次提交已将它们关闭。本建议保持开放,下一轮处理。
说明:本轮直接替换了启动时 sourcing 通道本身(step 级 BASH_ENV/SHELLOPTS 固定 + env -i 干净子进程,已在测试中固定);建议中『对 wrapper 植入 BASH_ENV』的行为探针仍留待下一轮。