Skip to content

feat(fix): cap automated review->fix cycles per PR - #6596

Closed
guyoron1 wants to merge 1 commit into
fullsend-ai:mainfrom
guyoron1:feat/fix-cycle-cap
Closed

feat(fix): cap automated review->fix cycles per PR#6596
guyoron1 wants to merge 1 commit into
fullsend-ai:mainfrom
guyoron1:feat/fix-cycle-cap

Conversation

@guyoron1

Copy link
Copy Markdown

Nothing bounds the review->fix loop today; a standing disagreement between the review and fix agents oscillates until a human notices the cost. Bot-triggered fixes now stop after REVIEW_MAX_FIX_CYCLES (default 3) bot-requested-changes reviews, with one explanatory comment. Human /fs-fix was never gated and remains the override.

What changed

  • .github/scripts/check-fix-eligibility.sh: after the existing label/author gates, counts CHANGES_REQUESTED reviews from the review bot (gh api .../pulls/{n}/reviews --paginate, aggregated the same way the neighboring fix-iteration count already does: --paginate | jq -s 'add | ...'). At or above REVIEW_MAX_FIX_CYCLES (default 3, 0 disables): emits ::warning::, posts one idempotent comment (marker <!-- fullsend-fix-cycle-cap -->, searched via the same paginate+jq pattern before posting), and exits 1. A failed reviews-API call warns and proceeds — cap unavailable does not mean fix forbidden. A non-numeric REVIEW_MAX_FIX_CYCLES warns and falls back to the default of 3 (mirrors route-review-model.sh's TRIVIAL_MAX_LINES handling, but falls back to a default instead of bailing out, since an unenforceable cap must not silently become no cap).
  • .github/workflows/reusable-dispatch.yml and .github/workflows/reusable-fix.yml: both existing "Check fix eligibility" steps now also pass REVIEW_MAX_FIX_CYCLES: ${{ vars.REVIEW_MAX_FIX_CYCLES }} through to the script (both invocation sites; check-fix-eligibility.sh is a single vendored source under internal/scaffold/vendormanifest.go's vendoredDefaultsScripts, byte-copied to .defaults/.github/scripts/ in consumer repos — no bundler step to keep in sync, unlike the agents repo's .src.sh/.sh split).
  • .github/scripts/check-fix-eligibility-test.sh: extends the existing stub-gh mock to also answer pulls/.../reviews and issues/.../comments (GET + POST), with a posted-comment-body.txt sentinel file so tests can assert whether a comment was (not) posted. Seven new cases: under cap proceeds, at cap exits 1 and posts a comment, marker-present skips the second comment, human trigger ignores the cap, 0 disables, reviews-API failure proceeds with a warning, non-numeric cap value warns and falls back to the default.

The review bot's REST login (<org>-review[bot]) is documented in docs/contributing/bot-identities.md; rather than threading a new env var for it, it's derived from SOURCE_REPO (already an input) the same way the REVIEW_BOT var is built in this same workflow's "Pre-fetch review body" step a few lines below — so no new plumbing was needed for that part.

Test plan

  • bash .github/scripts/check-fix-eligibility-test.sh — all 21 cases pass (14 pre-existing + 7 new)
  • Sanity-checked the 7 new cases against the pre-change script: 4 fail as expected (proving they exercise the new logic, not vacuous), 3 pass on both (the "doesn't wrongly block" cases)
  • shellcheck on both touched scripts — clean
  • actionlint on both touched workflow files, diffed against pristine origin/main (findings normalized for the line-number shift my one-line insertion causes) — identical 31 pre-existing job.workflow_sha/job.workflow_repository findings, zero new
  • Pre-commit hooks (shellcheck, actionlint, pinact, gitlint, etc.) pass

Note for reviewers

route-review-model.sh (the file this PR's non-numeric-handling style mirrors) does not exist on main yet — it ships in still-open draft PR #6590. I read it there for the style reference only; this PR does not depend on it and branches from main directly.

Nothing bounds the review->fix loop today; a standing disagreement
between the review and fix agents oscillates until a human notices the
cost. Bot-triggered fixes now stop after REVIEW_MAX_FIX_CYCLES (default
3) bot-requested-changes reviews, with one explanatory comment. Human
/fs-fix was never gated and remains the override.

Signed-off-by: guy oron <goron@redhat.com>
@github-actions

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

See E2E testing guide for details.

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ralphbean

Copy link
Copy Markdown
Member

/fs-review is it possible that the interaction cap already exists, but in the fullsend-ai/agents repo?

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 25, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 2:48 PM UTC · Completed 3:09 PM UTC

Commit: cad0fc8 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $7.08

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] .github/scripts/check-fix-eligibility.sh, .github/scripts/check-fix-eligibility-test.sh, .github/workflows/reusable-dispatch.yml, .github/workflows/reusable-fix.yml — All four changed files are under the .github/ protected path. This PR has no linked issue providing authorization for modifying governance/infrastructure files. Human approval is required for protected-path changes regardless of context.

  • [agent-reference-doc] docs/agents/fix.md:207 — The "Variables" section states "None." but this PR introduces REVIEW_MAX_FIX_CYCLES, a user-configurable variable (set via GitHub repo/org variables) that controls fix-cycle capping. This is the primary documentation for the fix agent and the most likely place an operator would look for tunable knobs.
    Remediation: Replace "None." with a table listing REVIEW_MAX_FIX_CYCLES: default 3, 0 disables, sourced from vars.REVIEW_MAX_FIX_CYCLES.

Medium

  • [commit-convention-violation] PR title — The title uses feat(fix) but per COMMITS.md, feat is reserved for end-user-visible features. This is an internal CI safety cap on automated review-fix cycles. COMMITS.md states: "Tightening internal heuristics ... that users don't directly control → fix." GoReleaser uses PR titles for release notes, so an incorrect feat pollutes the Features section.
    Remediation: Change title to fix(fix): cap automated review->fix cycles per PR.

  • [GHA workflow command injection] .github/scripts/check-fix-eligibility.sh:68REVIEW_MAX_FIX_CYCLES is interpolated into a ::warning:: annotation before numeric validation succeeds. At this point it contains the raw invalid value. The existing code sanitizes PR_IS_BOT and PR_LOGIN via _sanitize_for_annotation for the same reason — the new interpolation should follow the same defense-in-depth pattern.
    Remediation: Pass the value through _sanitize_for_annotation before the ::warning:: emission on line 68.

  • [fail-open] .github/scripts/check-fix-eligibility.sh:97 — When the reviews API call fails, the script proceeds without enforcing the cycle cap ("proceeding without the cap"). This is a fail-open pattern on a safety gate. A transient API failure silently bypasses the cap, undermining its purpose.
    Remediation: Consider failing closed (exit 1) on API failure, or add an explicit comment documenting the fail-open tradeoff and confirming it is intentional.

  • [dangling cross-reference] .github/scripts/check-fix-eligibility.sh:76 — The inline comment references route-review-model.sh's TRIVIAL_MAX_LINES handling as precedent, but neither exists in this repository. The PR body notes it ships in draft PR feat(review): route trivial diffs to a cheaper model #6590; if that PR is abandoned or modified, this comment becomes misleading.
    Remediation: Replace with self-contained rationale: "An unenforceable cap must not silently become no cap, so we warn and fall back to the default."

  • [agent-reference-doc] docs/agents/fix.md:84 — The "Iteration limits" section documents only agent-internal caps (5 bot / 10 human) but does not mention the new eligibility-level cycle cap, which defaults to 3 and fires earlier in the pipeline.
    Remediation: Add the cycle cap to the Iteration limits section, noting it fires at the eligibility-check level before the agent starts.

Low

  • [user-guide] docs/guides/user/bugfix-workflow.md:155 — The loop-stopping conditions list does not mention the new cycle cap. The existing "iteration cap" wording is imprecise but not incorrect.
    Remediation: Add the cycle cap as another stopping condition.

  • [edge-case] .github/scripts/check-fix-eligibility.sh:82 — If gh api --paginate returns a 200 with a completely empty body, jq -s 'add' produces null and .[] fails. In practice, GitHub's reviews endpoint always returns an array, and the failure path falls through safely.

  • [test-organization] .github/scripts/check-fix-eligibility-test.sh:120run_test now takes 11 positional arguments. Other test files in the repo cap at 4–5 args using separate setup functions. Optional middle parameters require empty-string placeholders at call sites.
    Remediation: Consider splitting cycle-cap test state into a separate setup step or environment variables.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

fi
fi

# Cap automated fix cycles: block further bot-triggered fixes once the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] GHA workflow command injection

REVIEW_MAX_FIX_CYCLES is interpolated into a ::warning:: annotation before numeric validation succeeds. At this point it contains the raw invalid value. The existing code sanitizes PR_IS_BOT and PR_LOGIN via _sanitize_for_annotation for the same reason. Although the source is vars.REVIEW_MAX_FIX_CYCLES (settable only by repo admins), the new interpolation should follow the same defense-in-depth pattern.

Suggested fix: Pass the value through _sanitize_for_annotation before the ::warning:: emission on line 68.

'add | [.[] | select(.state == "CHANGES_REQUESTED" and .user.login == $login)] | length'); then
if (( CYCLE_COUNT >= REVIEW_MAX_FIX_CYCLES )); then
echo "::warning::PR #${PR_NUM} has reached ${CYCLE_COUNT} automated fix cycles (cap ${REVIEW_MAX_FIX_CYCLES}) — a human needs to look"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] fail-open

When the reviews API call fails, the script proceeds without enforcing the cycle cap ('proceeding without the cap'). This is a fail-open pattern on a safety gate. A transient API failure silently bypasses the cap, undermining its purpose.

Suggested fix: Consider failing closed (exit 1) on API failure, or add an explicit comment documenting the fail-open tradeoff and confirming it is intentional.

# it already exited at the TRIGGER_SOURCE check above, before this gate.
REVIEW_MAX_FIX_CYCLES="${REVIEW_MAX_FIX_CYCLES:-3}"
if [[ ! "${REVIEW_MAX_FIX_CYCLES}" =~ ^[0-9]+$ ]]; then
# Mirrors route-review-model.sh's TRIVIAL_MAX_LINES handling, but falls

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] dangling cross-reference

The inline comment references route-review-model.sh's TRIVIAL_MAX_LINES handling as precedent, but neither exists in this repository. The PR body notes it ships in draft PR #6590; if that PR is abandoned or modified, this comment becomes misleading.

Suggested fix: Replace with self-contained rationale: 'An unenforceable cap must not silently become no cap, so we warn and fall back to the default.'

echo "::warning::REVIEW_MAX_FIX_CYCLES is not a number (${REVIEW_MAX_FIX_CYCLES}) — using default of 3"
REVIEW_MAX_FIX_CYCLES=3
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] edge-case

If gh api --paginate returns a 200 with a completely empty body, jq -s 'add' produces null and .[] fails. In practice, GitHub's reviews endpoint always returns an array, and the failure path falls through safely to the else branch.

# $5 — trigger source
# $6 — labels (optional, comma-separated)
# $7 — expected annotation substring (optional)
# $8 — reviews API response body (optional, default "[]"; "FAIL"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] test-organization

run_test now takes 11 positional arguments. Other test files in the repo cap at 4-5 args using separate setup functions. Optional middle parameters require empty-string placeholders at call sites.

Suggested fix: Consider splitting cycle-cap test state into a separate setup step or environment variables.

@ralphbean

Copy link
Copy Markdown
Member

I think this PR may be superseded by existing functionality in the agents repo. The agents repo already has iteration caps implemented in scripts/pre-fix.src.sh with ITERATION_CAP and ITERATION_CAP_HUMAN variables, though they're not currently documented.

Filed fullsend-ai/agents#1020 to document those variables. That might be a simpler solution than implementing a separate review-count-based cap here.

@guyoron1

Copy link
Copy Markdown
Author

Closing — this duplicates an existing mechanism I missed: the pre-fix script already enforces an iteration cap (ITERATION_CAP, bot-triggered default 5; ITERATION_CAP_HUMAN default 10), with the iteration derived from counting fullsend-fix commits on the PR, escalation messaging, and the needs-human label near the threshold (docs/agents/fix.md). The loop is already bounded; nothing further needed.

@guyoron1 guyoron1 closed this Aug 26, 2026
@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 26, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 4:21 AM UTC · Completed 4:38 AM UTC

Commit: cad0fc8 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $6.99

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #6596 — feat(fix): cap automated review→fix cycles per PR

Timeline

  1. 2026-08-25 14:32 — guyoron1 (external contributor) opened PR #6596 adding a CI-level cycle cap (REVIEW_MAX_FIX_CYCLES) to check-fix-eligibility.sh. The cap would block automated fix dispatch after 3 bot-requested-changes reviews, complementing the agent-internal iteration caps.
  2. 2026-08-25 14:46 — ralphbean commented /fs-review is it possible that the interaction cap already exists, but in the fullsend-ai/agents repo? — triggering the review agent while asking a strategic question about duplication.
  3. 2026-08-25 14:49–15:09 — Review agent ran (run 32861638477), dispatching 6 sub-agents (all Opus), producing 10 findings and requesting changes. Cost: $7.08, runtime: ~22 min.
  4. 2026-08-25 (later) — ralphbean noted that ITERATION_CAP / ITERATION_CAP_HUMAN already exist in the agents repo's scripts/pre-fix.src.sh and filed fullsend-ai/agents#1020 to document them.
  5. 2026-08-26 04:14 — guyoron1 closed the PR as duplicative of the existing agent-level mechanism.

Review quality assessment

The review agent produced 10 findings, all factually grounded. The GHA workflow command injection finding, dangling cross-reference to non-existent route-review-model.sh, and docs-currency gaps were particularly valuable. Severity calibration was mostly appropriate. One finding had an incorrect remediation: the [commit-convention-violation] correctly identified feat(fix) as wrong per COMMITS.md but suggested fix(fix) when ci(fix) would be more appropriate for a CI safety mechanism.

The review agent did not and could not answer ralphbean's strategic question about whether existing caps already solve this problem — the decisive insight that led to PR closure.

What went well

  • Review finding accuracy: 10/10 findings were factually correct. The challenger sub-agent appropriately removed 3 findings and downgraded 2.
  • Human review: ralphbean's strategic question about the agents repo's existing ITERATION_CAP was the most valuable contribution and led to efficient resolution.
  • No wasted agent cycles: No fix agent was triggered, so the only agent cost was the single review ($7.08).

Nuance on the "duplicate" assessment

The PR's CI-level cap (REVIEW_MAX_FIX_CYCLES, counting CHANGES_REQUESTED reviews, default 3) and the agents repo's cap (ITERATION_CAP, counting fix commits, default 5) operate at different layers — the former blocks dispatch before the agent starts, while the latter limits iterations within the agent sandbox. They are arguably complementary, not duplicate. The PR author accepted the duplicate assessment and closed, which was a reasonable practical decision even if the technical distinction exists.

Evidence for existing open issues

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants