Skip to content

Commit e933f13

Browse files
authored
fix(test-classifier): raise turn budget + treat max-turns overflow as soft outcome (#79)
* fix(test-classifier): raise turn budget + treat max-turns overflow as soft outcome Cold-runner suite bootstrap (Ruby+bundle, Postgres, asset precompile) could consume the whole agentic turn budget before the agent emitted a verdict, tripping "Reached max turns" and failing the job red. - Raise AI_SUITE_MAX_TURNS default 40 -> 80 for bootstrap headroom. - Grant Grep,Glob,Task,Agent so read-only search batches in parallel and large-diff discovery can be delegated to a subagent (one parent turn). - Enable codex network access in workspace-write so bundle/npm/pip installs work in suite mode (read-only triage keeps the no-network default). - Detect turn-budget overflow (ai_review::is_max_turns) and degrade to a non-blocking soft outcome (exit 0) instead of failing the build. - Document budget discipline in SKILL.md / dispatcher: reuse the repo's own bootstrap entry point or CI recipe, batch lookups, run the suite once blocking, and emit a best-effort verdict before overflowing. * docs(test-classifier): trim overlong inline comments and prompt prose
1 parent 45cad6b commit e933f13

3 files changed

Lines changed: 83 additions & 7 deletions

File tree

testing/classifier/.skills/_lib/ai-classifier-dispatch.sh

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ _AI_CLASSIFIER_DISPATCH_LOADED=1
6666
# read-only, INFERRED-only pass.
6767
AI_RUN_SUITE="${AI_RUN_SUITE:-1}"
6868

69-
# Bounds for the agent loop when it is running the suite, so a runaway
70-
# install/test cycle can't hang the job. The timeout wraps the whole CLI call;
71-
# --max-turns (claude) caps agentic iterations.
69+
# Bounds for the agent loop when running the suite. The timeout wraps the whole
70+
# CLI call; --max-turns caps agentic iterations. The budget must cover both a
71+
# cold-runner bootstrap (toolchain, DB, assets) and the classification — 40 was
72+
# too low (bootstrap alone hit the cap). Overflow is a soft outcome now (see
73+
# ai_review::is_max_turns), not a hard failure.
7274
AI_SUITE_TIMEOUT_SECS="${AI_SUITE_TIMEOUT_SECS:-1500}" # 25 min hard ceiling
73-
AI_SUITE_MAX_TURNS="${AI_SUITE_MAX_TURNS:-40}"
75+
AI_SUITE_MAX_TURNS="${AI_SUITE_MAX_TURNS:-80}"
7476

7577
# ── Color helpers (suppressed in CI / non-TTY) ──────────────────────────────
7678
if [[ -t 1 ]] && [[ "${CI:-}" != "true" ]] && [[ "${NO_COLOR:-}" == "" ]]; then
@@ -494,6 +496,10 @@ ai_review::invoke_claude() {
494496
# this flag set is required for suite-running, not optional. --allowedTools
495497
# scopes it to exactly what the task needs; --max-turns bounds the loop.
496498
#
499+
# Grep,Glob batch in parallel within one turn (vs a `bash grep` per turn);
500+
# Task/Agent (subagent tool, both names) lets a whole discovery pass cost
501+
# the parent ONE turn. WebSearch/WebFetch stay ungranted (data minimization).
502+
#
497503
# NOTE: no 2>&1 — keep the agent's stderr diagnostics OUT of the captured
498504
# stdout so they can't corrupt the JSON/marker parse. stderr still flows to
499505
# the terminal / CI log.
@@ -503,14 +509,14 @@ ai_review::invoke_claude() {
503509
# there is no stdin to read.
504510
$(ai_review::timeout_prefix) claude -p "${SKILL_PROMPT}" \
505511
--permission-mode bypassPermissions \
506-
--allowedTools "Bash,Read,Edit" \
512+
--allowedTools "Bash,Read,Edit,Grep,Glob,Task,Agent" \
507513
--max-turns "${AI_SUITE_MAX_TURNS}" \
508514
--output-format stream-json --verbose < /dev/null \
509515
| ai_review::stream_split
510516
else
511517
$(ai_review::timeout_prefix) claude -p "${SKILL_PROMPT}" \
512518
--permission-mode bypassPermissions \
513-
--allowedTools "Bash,Read,Edit" \
519+
--allowedTools "Bash,Read,Edit,Grep,Glob,Task,Agent" \
514520
--max-turns "${AI_SUITE_MAX_TURNS}"
515521
fi
516522
else
@@ -541,15 +547,24 @@ ai_review::invoke_codex() {
541547
# folded into the captured stdout that gets parsed.
542548
local stream="${AI_REVIEW_DO_STREAM:-0}"
543549
local sandbox="read-only"
544-
(( AI_RUN_SUITE == 1 )) && sandbox="workspace-write"
550+
local -a codex_cfg=()
551+
if (( AI_RUN_SUITE == 1 )); then
552+
sandbox="workspace-write"
553+
# workspace-write allows file writes but not network by default, so dep
554+
# installs (bundle/npm/pip) fail without this. Enable it only for suite
555+
# mode; read-only triage keeps the no-network default.
556+
codex_cfg=(-c sandbox_workspace_write.network_access=true)
557+
fi
545558

546559
if (( stream == 1 )); then
547560
ai_review::info "Streaming the agent's steps below (set AI_REVIEW_STREAM=0 to silence)…" >&2
548561
$(ai_review::timeout_prefix) codex exec --json --sandbox "${sandbox}" \
562+
"${codex_cfg[@]+"${codex_cfg[@]}"}" \
549563
--skip-git-repo-check "${SKILL_PROMPT}" \
550564
| ai_review::codex_stream_split
551565
else
552566
$(ai_review::timeout_prefix) codex exec --sandbox "${sandbox}" \
567+
"${codex_cfg[@]+"${codex_cfg[@]}"}" \
553568
--skip-git-repo-check "${SKILL_PROMPT}" 2>&1
554569
fi
555570
}
@@ -674,6 +689,17 @@ ai_review::invoke_ai() {
674689
esac
675690
}
676691

692+
# ── Turn-budget overflow detection ──────────────────────────────────────────
693+
# When the agent exhausts --max-turns it exits non-zero with a recognizable
694+
# banner instead of our marker. That's recoverable, not a real failure — the
695+
# caller degrades to a non-blocking soft outcome. The match is broad/case-
696+
# insensitive across CLIs; a false positive only softens a hard failure, which
697+
# is the safe direction.
698+
ai_review::is_max_turns() {
699+
local output="$1"
700+
grep -qiE 'reached max turns|max[ _-]?turns|turn limit' <<< "${output}"
701+
}
702+
677703
# ── Result marker parsing ───────────────────────────────────────────────────
678704
# The canonical marker is: <<<AI_REVIEW_RESULT:CLASSIFIED|NO_ACTION>>>
679705
# We grep for the structured form; if absent we return UNPARSEABLE so the
@@ -752,6 +778,13 @@ ai_review::run() {
752778
ai_review::log "────────────────────────────────────────────────────────────"
753779

754780
if (( invoke_rc != 0 )); then
781+
# Turn-budget overflow is a soft outcome: no verdict, but exit 0 rather than
782+
# failing the build. Bump AI_SUITE_MAX_TURNS or set AI_RUN_SUITE=0.
783+
if ai_review::is_max_turns "${review_output}"; then
784+
ai_review::warn "${SKILL_HUMAN_NAME}: agent hit the turn budget (AI_SUITE_MAX_TURNS=${AI_SUITE_MAX_TURNS}) before finishing — no verdict emitted this run."
785+
ai_review::warn " This is non-blocking. Bump AI_SUITE_MAX_TURNS for more headroom, or run with AI_RUN_SUITE=0 (--no-run-suite) for a fast diff-only (INFERRED) pass."
786+
exit 0
787+
fi
755788
ai_review::err "AI CLI (${AI_REVIEW_TOOL_RESOLVED}) exited with code ${invoke_rc}."
756789
if (( AI_REVIEW_NO_BLOCK == 1 )); then
757790
ai_review::warn "--no-block in effect: not failing despite CLI error."

testing/classifier/.skills/test-classifier/SKILL.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,30 @@ classify the failures you actually observe:
129129
your OBSERVED signal.
130130
4. Set the result `mode` to `"OBSERVED"` in the JSON.
131131

132+
> **Spend your turn budget on classification, not on rediscovering the
133+
> environment.** You have a bounded number of agentic turns
134+
> (`AI_SUITE_MAX_TURNS`), and a turn is one assistant iteration, not one tool
135+
> call — batch independent calls into one turn. Trial-and-error bootstrap on a
136+
> cold runner can burn the whole budget before you emit a verdict, so:
137+
>
138+
> - **Reuse the repo's own setup.** Prefer its bootstrap entry point
139+
> (`bin/setup`, `script/bootstrap`, a Makefile `setup` target); otherwise
140+
> reproduce the CI workflow (`.github/workflows/*.yml` — setup steps, service
141+
> containers, version pins, test command) rather than guessing. If CI
142+
> precompiles assets, do that too (a suite that skips it fails with an
143+
> `ENVIRONMENT_ISSUE`, not an app bug).
144+
> - **Batch.** Chain setup steps with `&&` in one command (shell state doesn't
145+
> persist across calls); issue independent read-only lookups together.
146+
> - **Run the suite once, blocking** — don't background it and poll with
147+
> `sleep`/`pgrep`.
148+
> - **Delegate large-diff discovery to a subagent if you have one** (Claude
149+
> Code's `Task`/`Agent`): its whole exploration costs the parent ONE turn.
150+
> Give it the paths/questions it needs (it can't see your history). Codex and
151+
> Copilot have none — keep discovery serial but batched.
152+
> - **Degrade before you overflow.** Near the cap, emit your best verdict from
153+
> what you've observed (`confidence: low` beats no output); if bootstrap can't
154+
> finish, fall back to `INFERRED` and say so.
155+
132156
> **Infrastructure-as-code tests need a teardown guarantee — never strand real
133157
> resources.** Some repos (Terraform/OpenTofu modules, Pulumi, CloudFormation)
134158
> have a "test suite" that **applies real cloud resources**. That is fine to run

testing/classifier/.skills/test-classifier/scripts/test-classifier-dispatcher.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,17 @@ if [[ "${AI_RUN_SUITE:-1}" == "1" ]]; then
400400
cannot locate/install/run it (no suite, missing toolchain, needs services,
401401
times out), fall back to predicting from the diff, set "mode":"INFERRED",
402402
and state the reason in "summary".
403+
404+
BUDGET DISCIPLINE — you have a bounded number of agentic turns; a turn is
405+
one assistant iteration, not one tool call, so batch independent lookups
406+
and chain setup steps with && into one command. Derive the bootstrap from
407+
the repo FIRST (its own setup entry point, else its CI workflow) instead
408+
of guessing package managers by trial and error. Run the suite ONCE as a
409+
blocking command — don't poll with sleep/pgrep. For a LARGE diff, if you
410+
have a subagent tool (Claude Code Task/Agent), delegate discovery to it
411+
(one parent turn; give it the paths/questions — it can't see your
412+
history); codex/copilot have none, so keep discovery serial but batched.
413+
Near the budget, STOP and emit your best verdict rather than overflowing.
403414
SIGNAL
404415
else
405416
read -r -d '' SIGNAL_STEP <<'SIGNAL' || true
@@ -1048,6 +1059,14 @@ test_classifier::run() {
10481059
fi
10491060

10501061
if (( invoke_rc != 0 )); then
1062+
# Turn-budget overflow is a soft outcome: no verdict, but exit 0 so the PR
1063+
# check stays green-advisory rather than failing red. Bump AI_SUITE_MAX_TURNS
1064+
# or set AI_RUN_SUITE=0 for a diff-only (INFERRED) pass.
1065+
if ai_review::is_max_turns "${classifier_output}"; then
1066+
ai_review::warn "AI Test Classifier: agent hit the turn budget (AI_SUITE_MAX_TURNS=${AI_SUITE_MAX_TURNS}) before emitting a verdict — no classification this run (non-blocking)."
1067+
ai_review::warn " Bump AI_SUITE_MAX_TURNS for more headroom, or set AI_RUN_SUITE=0 (--no-run-suite) for a fast diff-only (INFERRED) pass."
1068+
exit 0
1069+
fi
10511070
ai_review::err "AI CLI (${AI_REVIEW_TOOL_RESOLVED}) exited with code ${invoke_rc}."
10521071
exit 1
10531072
fi

0 commit comments

Comments
 (0)