Skip to content

feat: add fix agent for automated PR review remediation - #337

Merged
ascerra merged 9 commits into
fullsend-ai:mainfrom
ascerra:feat/fix-agent
Apr 29, 2026
Merged

feat: add fix agent for automated PR review remediation#337
ascerra merged 9 commits into
fullsend-ai:mainfrom
ascerra:feat/fix-agent

Conversation

@ascerra

@ascerra ascerra commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the fix agent — the fourth agent in the fullsend pipeline. When the review agent requests changes on a code agent PR (or a human posts /fix), the fix agent reads the review findings, implements fixes, runs tests, and pushes a new commit. This closes the review → fix loop without human intervention for straightforward feedback.

What's included

Agent definition and skill (agents/fix.md, skills/fix-review/)

  • Structured workflow: read review → plan fixes → implement → test → scan secrets → commit → produce structured output
  • Strategy escalation at iteration 3 (broader context, alternative approaches)
  • Explicit disagreement handling — the agent can decline a finding with a documented reason rather than blindly implementing everything

Sandbox and security (policies/fix.yaml, harness/fix.yaml)

  • Reuses coder app credentials (no new GitHub App) with distinct git author name (fullsend-fix) for attribution
  • Dual-token isolation: read-only token in sandbox, write token only in runner post-script
  • Harness-level validation_loop for fix-result.schema.json (matching the review agent pattern)
  • 1 MB review body length cap in workflow (defense-in-depth against oversized inputs)
  • Dual WIF/SA-key auth using vars.FULLSEND_GCP_AUTH_MODE == 'wif' (aligned with all other agent workflows post-fix: use vars instead of secrets in workflow if conditions #484)
  • OIDC token host_file entry with optional: true

Pre/post scripts (scripts/pre-fix.sh, scripts/post-fix.sh)

  • Pre-script: validates inputs, checks iteration cap
  • Post-script: protected-path check → authoritative secret scan (gitleaks) → pre-commit hooks → push → process structured output → post PR summary comment
  • All validation scoped to ${PRE_AGENT_HEAD}..HEAD (agent's commits only, not entire branch — handles multi-commit validation_loop retries)
  • Protected-path check uses bash prefix matching (not grep regex)
  • Plain git push (no --force-with-lease) since agents never amend commits (post-code.sh: replace --force-with-lease with plain git push #411)
  • Exit code 2 from process-fix-result.py trapped so labels/summary sections still run

Dual iteration caps (env/fix-agent.env, scripts/pre-fix.sh, scripts/post-fix.sh)

  • Bot-triggered runs (review→fix loop) capped at ITERATION_CAP=5
  • Human-triggered /fix commands capped at ITERATION_CAP_HUMAN=10
  • When the bot cap is reached, needs-human label is added and the error message tells the human they can still use /fix
  • needs-human label only applied on bot-triggered runs (not human /fix runs)
  • Ensures humans are never locked out after a bot loop exhausts its budget

Structured output (schemas/fix-result.schema.json, scripts/process-fix-result.py)

  • JSON schema for documenting fixes applied, findings disagreed with, and test results
  • Conditional required fields: fix → requires description, disagree → requires reason
  • minItems: 1 on actions array — empty results are rejected
  • decision_points items require description and rationale
  • strategy_change field rendered in summary comment when present (blockquote format)
  • Python processor posts a formatted summary comment on the PR (returns exit 2 on failure, documented in docstring)
  • Secret scan of structured output before posting (defense against exfiltration via prompt injection)
  • Unit test for return 2 path via subprocess.run mock
  • Unit tests for strategy_change rendering (present and absent cases)

Shim workflow updates (templates/shim-workflow.yaml)

  • dispatch-fix-bot: triggers on changes_requested review from the org's review bot (exact match via format('{0}-review[bot]', github.repository_owner))
  • dispatch-fix-human: triggers on /fix command from OWNER/MEMBER/COLLABORATOR; excludes bot users (user.type != 'Bot'); blocks fork PRs via API check with PR_URL in env: block (not inline ${{ }})
  • Both fix dispatch jobs build minimal payloads with jq -cn containing only fields the downstream workflow needs — prevents exceeding GitHub's 65KB workflow_dispatch input limit on large dependency-update PRs
  • Cross-cancellation concurrency so human /fix preempts bot-triggered runs
  • dispatch-review no longer re-triggers on pull_request_review events (only pull_request_target, /review, ready-for-review label)
  • Reverted stale auto-triage expansion (aligned with Disable automatic triage on issue open/edit — require /triage command #394 on main)

Workflow (fix.yml)

  • Review body fetch filtered to review bot only (.user.login == "${REVIEW_BOT}" with REVIEW_BOT constructed from github.repository_owner) — prevents injection via racing CHANGES_REQUESTED reviews
  • Fail-safe iteration counter: API failure defaults to ITERATION_CAP (blocks run) instead of 0 (silent reset)
  • Iteration count TOCTOU race documented inline — cancel-in-progress mostly prevents, allows at most +1 overshoot
  • PRE_AGENT_HEAD recorded after checkout, passed via runner_env to scope post-script validation
  • prepare-sandbox-credentials.sh step added
  • WIF/SA-key conditional uses vars.FULLSEND_GCP_AUTH_MODE (aligned with fix: use vars instead of secrets in workflow if conditions #484 — GitHub Actions rejects secrets in step if: expressions)

Tests

  • E2e assertion list updated with all 10 fix-agent scaffold files
  • process-fix-result-test.py unit tests for the structured output processor (including exit 2 path, strategy_change rendering)
  • workflows_test.go uses len(managedFiles) instead of hardcoded counts
  • validate-output-schema-test.sh tests for FULLSEND_OUTPUT_FILE override, path traversal guard, allOf/if/then conditional rules (fix missing description, disagree missing reason, empty actions), and minItems enforcement

Tested on ascerra-fullsend-lab

Dual iteration cap verification (PR #3)

Run Trigger Iteration Cap used Result
25113328340 bot 5 of 5 bot=5 Passed — needs-human label added, agent ran
25113706269 human 6 of 10 human=10 Passed — human /fix ran at iteration 6 (above bot cap)
25114007809 bot 7 > 5 bot=5 Blocked — pre-fix rejected: "A human can still direct the agent with /fix (up to 10 total iterations)"

Round 5 — minimal payload dispatch

Run 25085229338 (PR #3, 2m45s):
Dispatched with new minimal jq -cn payload (only pull_request.number, .head.ref, .head.repo.full_name, .base.ref, .base.repo.full_name). All steps passed: PR context extraction parsed minimal payload correctly → fork check → SA-key auth → sandbox agent ran → schema validation passed (with minItems: 1) → process-fix-result.py posted summary (iteration 5).

Prior end-to-end runs

Full pipelinerun 25077774604 (PR #4, 3m43s):
Mock changes_requested review → shim dispatch → fork check → review body fetch (exact bot match) → SA-key auth (WIF skipped correctly) → sandbox agent ran 98s → schema validation passed → gitleaks scan clean → git push → summary comment posted. All green.

Iteration cap enforcementrun 25077000049 (PR #3):
FIX_ITERATION=5 of 5 — pre-script correctly warned "approaching cap", and the pipeline infrastructure (shim, auth, review body fetch, pre-agent HEAD recording) all passed before the sandbox ran.

Human-triggeredPR #3:
/fix replace the fire emoji with a different emoji just make sure its red → fix agent chose red heart → pushed commit → posted structured summary with decision rationale.

Issues filed during review

Closes #197

Made-with: Cursor

@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown

Site preview

Preview: https://7162a0ba-site.fullsend-ai.workers.dev

Commit: 730e13831f5b3deac6120f8e878659fbca3f588a

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review summary

Overall this is well-structured and follows established patterns. Three items need fixing before merge, plus one deferred note with a follow-up issue.

Request changes (3):

  1. Fork check uses github.token which lacks cross-repo permissions for private source repos
  2. Truncation warning logs the wrong (post-truncation) length
  3. E2e test file list doesn't cover any fix-agent scaffold files

Noted, deferred (1):
4. Concurrency group can collapse to a global group on malformed payloads (pre-existing pattern across all agent workflows — tracked in follow-up issue)

Comment thread internal/scaffold/fullsend-repo/.github/workflows/fix.yml
Comment thread internal/scaffold/fullsend-repo/scripts/process-fix-result.py
Comment thread internal/scaffold/fullsend-repo/.github/workflows/fix.yml
Comment thread internal/layers/workflows_test.go
ascerra added a commit to ascerra/fullsend that referenced this pull request Apr 23, 2026
1. Use sandbox token (scoped to source repo) for fork check and PR
   context extraction instead of github.token, which lacks cross-repo
   permissions for private source repos.
2. Capture original body length before truncation so the warning log
   reports the correct pre-truncation size.
3. Add fix-agent scaffold files to e2e assertion list so the install
   flow is verified end-to-end.

Made-with: Cursor
@ralphbean

Copy link
Copy Markdown
Member

Just needs conflict resolution on internal/layers/workflows_test.go and rebase on main.

@ralphbean
ralphbean self-requested a review April 23, 2026 11:40

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good overall architecture — the fix agent follows established patterns well, with proper token isolation, fork PR blocking, protected-path enforcement, and secret scanning of structured output.

Two items need fixing before merge; six more noted for follow-up.

Comment thread internal/scaffold/fullsend-repo/.github/workflows/fix.yml Outdated
Comment thread internal/scaffold/fullsend-repo/env/fix-agent.env
Comment thread internal/scaffold/fullsend-repo/templates/shim-workflow.yaml Outdated
Comment thread internal/scaffold/fullsend-repo/env/fix-agent.env
Comment thread internal/scaffold/fullsend-repo/scripts/post-fix.sh Outdated
Comment thread internal/scaffold/fullsend-repo/scripts/post-fix.sh
Comment thread internal/scaffold/fullsend-repo/scripts/process-fix-result.py Outdated
Comment thread internal/scaffold/fullsend-repo/scripts/post-fix.sh Outdated
ascerra added 2 commits April 24, 2026 06:27
Introduces the fix agent that iterates on PRs based on review agent
findings or human /fix commands. Reuses the coder app credentials
(no new GitHub App). Includes sandbox policy, structured JSON output
schema, pre/post scripts with gitleaks scanning, iteration cap (5)
with strategy escalation (at 3), and cross-cancellation concurrency
so human /fix commands preempt bot runs.

Closes fullsend-ai#197

Made-with: Cursor
1. Use sandbox token (scoped to source repo) for fork check and PR
   context extraction instead of github.token, which lacks cross-repo
   permissions for private source repos.
2. Capture original body length before truncation so the warning log
   reports the correct pre-truncation size.
3. Add fix-agent scaffold files to e2e assertion list so the install
   flow is verified end-to-end.

Made-with: Cursor
ascerra added a commit to ascerra/fullsend that referenced this pull request Apr 24, 2026
1. Use random heredoc delimiter for GITHUB_OUTPUT instruction to prevent
   delimiter injection from /fix comment body.
2. Fix Pushed status line to correctly print "yes"/"no" instead of
   printing "false" on success.

Made-with: Cursor

@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.

Review: #337

Head SHA: 6d32581
Timestamp: 2026-04-24T00:00:00Z
Outcome: request-changes

Summary

This PR introduces a well-structured fix agent that follows established patterns (dual-token isolation, protected-path checks, gitleaks scanning, disallowedTools). The agent definition, skill, structured output schema, pre/post scripts, and Python result processor are all solidly built. However, the shim workflow template is missing the pull_request_review event in its on: trigger list, which means neither the bot-triggered fix dispatch nor the review-on-approval dispatch will ever fire — this is a critical correctness issue that must be resolved before merging.

Findings

Critical

  • [Correctness] internal/scaffold/fullsend-repo/templates/shim-workflow.yaml:14-20 — The on: trigger block does not include pull_request_review as an event type. The PR adds two new jobs (dispatch-fix-bot at line 119 and the modified dispatch-review at line 87) that depend on github.event_name == 'pull_request_review', but this event will never reach the workflow because it is not listed in the trigger. Both the bot-triggered fix dispatch and the review-on-non-changes-requested dispatch are dead code.
    Remediation: Add pull_request_review: types: [submitted] to the on: block alongside the existing issues, issue_comment, and pull_request_target triggers.

Medium

  • [Correctness] internal/scaffold/fullsend-repo/env/fix-agent.env:14 — The git author email is fullsend-code@users.noreply.github.com, shared with the code agent. While the GIT_AUTHOR_NAME is correctly differentiated (fullsend-fix vs fullsend-code), the shared email could cause confusion in commit attribution and makes the fix agent's commits less distinguishable in email-based workflows. Consider using fullsend-fix@users.noreply.github.com for clarity.

  • [Correctness] internal/scaffold/fullsend-repo/templates/shim-workflow.yaml:87-105 — The modified dispatch-review condition now references pull_request_review events, but since the review agent's dispatch was previously triggered only by pull_request_target for auto-review, this condition change has no current effect (beyond being future-proofing for when pull_request_review is added to triggers). However, the logic github.event.review.state != 'changes_requested' could unintentionally dispatch the review agent on approved or commented review events once the trigger is added. Consider whether this is the desired behavior — it could cause infinite review-on-approval loops if not carefully scoped.

Low

  • [Style/conventions] internal/scaffold/fullsend-repo/scripts/post-fix.sh — The RUN_DIR variable is captured and used to locate fix-result.json but is not documented in the "Required environment variables" header comment. Minor documentation gap.

  • [Style/conventions] internal/scaffold/fullsend-repo/scripts/process-fix-result.py:1 — The script uses #!/usr/bin/env python3 shebang but is not marked executable (no chmod +x in the diff). The Makefile invokes it with python3 explicitly so this is not a functional issue, but the shebang implies direct execution.

Info

  • [Intent alignment] The issue (#197) specifies "configurable reviewer whitelist" for which bot logins trigger the fix agent. The shim currently uses a broad endsWith(github.event.review.user.login, '[bot]') check. This is a reasonable MVP simplification but does not match the acceptance criterion for a pluggable whitelist. Tracked as future work.

  • [Intent alignment] The issue specifies workflow_dispatch with pr_number + instruction as a manual fallback trigger in the shim. The fix.yml workflow supports these inputs, but the shim does not include a workflow_dispatch dispatch path for the fix agent. This appears intentional (users can call fix.yml directly), but worth confirming.

  • [Platform security] Token isolation is correctly implemented: the sandbox receives a read-only token, and PUSH_TOKEN only exists in the runner environment for the post-script. The disallowedTools list correctly blocks gh api, git push, and mutation commands. Defense-in-depth layers (protected-path check, gitleaks scan, pre-commit) match the established code agent pattern.

Footer

Outcome: request-changes
This review applies to SHA 6d325817b35311987ed4e143eadd329af2cf5733. Any push to the PR head clears this review and requires a new evaluation.

ascerra added a commit to ascerra/fullsend that referenced this pull request Apr 24, 2026
1. Use random heredoc delimiter for GITHUB_OUTPUT instruction to prevent
   delimiter injection from /fix comment body.
2. Fix Pushed status line to correctly print "yes"/"no" instead of
   printing "false" on success.
3. Switch process-fix-result.py from --body to --body-file - (stdin)
   to avoid exposing comment content in /proc/pid/cmdline.
4. Switch post-fix.sh from ls -t to glob-order loop for finding
   fix-result.json, matching post-triage.sh pattern.
5. Add comments documenting intentional design choices:
   - changes_requested exclusion from review dispatch (shim-workflow)
   - fullsend-fix author name with fullsend-code email (fix-agent.env)
   - plain git push without --force-with-lease (post-fix.sh)

HUMAN_INSTRUCTION expand:true risk deferred to fullsend-ai#408.

Made-with: Cursor

@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.

Review: #337

Head SHA: 780948b
Timestamp: 2026-04-24T00:00:00Z
Outcome: comment-only

Summary

The fix agent implementation follows the established agent patterns (code agent, review agent) closely and correctly — token isolation, sandbox policy, protected-path checks, defense-in-depth post-script, and structured output are all well-implemented. The core fix agent pipeline (workflow → pre-script → sandbox → post-script) is sound. Three medium findings relate to scope creep in the shim workflow (auto-triage on issue open/edit is unrelated to the fix agent), a missing reviewer bot whitelist (any bot can trigger the fix agent, not just the fullsend review bot), and a potential for wasteful double-dispatch of the review agent after fix agent pushes.

Findings

Medium

  • [Intent alignment] templates/shim-workflow.yaml — The dispatch-triage condition is expanded to auto-trigger on issues.opened and issues.edited events, and the on.issues.types array is changed from [labeled] to [opened, edited, labeled]. This is a separate feature unrelated to the fix agent (issue #197). Bundling it here makes it harder to review, revert, or attribute. Consider splitting this into its own PR.
    Remediation: Move the triage trigger expansion to a separate PR.

  • [Platform security] templates/shim-workflow.yaml:173-176 — The dispatch-fix-bot condition triggers on any bot account (endsWith(github.event.review.user.login, '[bot]')) that submits a changes_requested review. The linked issue (#197) specifies a "pluggable reviewer whitelist" that restricts which bot logins can trigger the fix agent. Without this whitelist, any GitHub App with review permissions on an enrolled repo can trigger the fix agent, potentially causing the agent to implement malicious review feedback. The defense-in-depth layers (protected paths, secret scan, pre-commit) mitigate severe outcomes, but a bot could still trick the agent into introducing logic bugs or subtle backdoors in non-protected paths.
    Remediation: Implement the reviewer whitelist from the issue spec (configurable in .fullsend/config.yaml), or at minimum hardcode the expected bot login (e.g., fullsend-review[bot]) as an interim check.

  • [Correctness] templates/shim-workflow.yaml:157-158 — The new dispatch-review condition adds pull_request_review events (excluding changes_requested). When the review agent posts its own review (approve or comment), that submission fires pull_request_review.submitted, which re-dispatches dispatch-review — creating a wasteful loop. Additionally, when the fix agent pushes a commit, both pull_request_target.synchronize and the subsequent review submission will dispatch the review agent, causing double dispatches. The review workflow's concurrency group likely prevents actual double execution, but it wastes Actions minutes on cancelled runs.
    Remediation: Add a bot-exclusion filter to the dispatch-review condition for pull_request_review events (e.g., && !endsWith(github.event.review.user.login, '[bot]')) to prevent the review agent from re-triggering itself.

Low

  • [Correctness] scripts/process-fix-result.py — The script does not validate the JSON against fix-result.schema.json. Unknown action types are logged as warnings but silently ignored, which is good for resilience, but schema validation would catch malformed output earlier and prevent subtle bugs in the summary comment.
    Remediation: Add schema validation (e.g., jsonschema library or a jq-based check) as a pre-processing step, or at minimum validate required fields.

Info

  • [Style/conventions] internal/layers/workflows_test.go — Replacing hardcoded file counts (36, 37) with len(managedFiles) and len(managedFiles)-1 is a good improvement that prevents future test breakage when scaffold files are added or removed.

  • [Correctness] The fix agent correctly reuses the coder app identity (FULLSEND_CODER_CLIENT_ID / FULLSEND_CODER_APP_PRIVATE_KEY) with distinct git author name fullsend-fix for iteration counting — clean separation without requiring a new GitHub App.

  • [Platform security] The random heredoc delimiter for HUMAN_INSTRUCTION (INSTRUCTION_$(openssl rand -hex 8)) is a solid defense against heredoc injection from /fix command content. The secret scan of fix-result.json before posting as a PR comment is a good defense against exfiltration via structured output.

Footer

Outcome: comment-only
This review applies to SHA 780948b2cd63294e2b2854ad24133cdc334f52c0. Any push to the PR head clears this review and requires a new evaluation.

ascerra added a commit to ascerra/fullsend that referenced this pull request Apr 24, 2026
1. Use random heredoc delimiter for GITHUB_OUTPUT instruction to prevent
   delimiter injection from /fix comment body.
2. Fix Pushed status line to correctly print "yes"/"no" instead of
   printing "false" on success.
3. Switch process-fix-result.py from --body to --body-file - (stdin)
   to avoid exposing comment content in /proc/pid/cmdline.
4. Switch post-fix.sh from ls -t to glob-order loop for finding
   fix-result.json, matching post-triage.sh pattern.
5. Add comments documenting intentional design choices:
   - changes_requested exclusion from review dispatch (shim-workflow)
   - fullsend-fix author name with fullsend-code email (fix-agent.env)
   - plain git push without --force-with-lease (post-fix.sh)

HUMAN_INSTRUCTION expand:true risk deferred to fullsend-ai#408.

Made-with: Cursor
@ascerra

ascerra commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

Addressing findings from the review bot (review on 780948b), fixed in 3107f6c:

Medium 1 — triage scope creep: Reverted. The issues.opened/edited auto-triage was stale code from before #394 landed on main. Removed the expanded on.issues.types and the dispatch-triage conditions — now matches upstream/main (triage only via /triage command or needs-info response).

Medium 2 — bot whitelist: Fixed. Replaced the broad endsWith(…, '[bot]') check in dispatch-fix-bot with an exact match: github.event.review.user.login == format('{0}-review[bot]', github.repository_owner). This derives the expected review bot login from the ExpectedAppSlug convention (<org>-review) without needing a config file. Only the org's own review bot can trigger the fix agent. Hardcoded convention vs configurable whitelist trade-off noted on #197.

Medium 3 — review agent self-trigger loop: Fixed. Removed the pull_request_review condition from dispatch-review entirely. The review agent is now only dispatched by pull_request_target (auto-review on push), /review command, or ready-for-review label. Human approved/commented reviews no longer re-trigger the review agent — re-reviewing after a human approves would be wasteful and confusing.

Low 1 — schema validation: Tracked in #412. Also added validation_loop for fix-result.schema.json in harness/fix.yaml (matching the review agent's harness pattern) and made validate-output-schema.sh configurable via FULLSEND_OUTPUT_FILE env var.

@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.

Review: #337

Head SHA: b690f3b
Timestamp: 2026-04-24T00:00:00Z
Outcome: comment-only

Summary

This PR introduces a well-structured fix agent that follows the established patterns from the code agent pipeline. The security architecture is sound: token isolation is correctly enforced (read-only GH_TOKEN in sandbox, PUSH_TOKEN restricted to runner_env), the protected-path check mirrors the code agent's defense, and the gitleaks scan of fix-result.json before posting as a PR comment is a good defense against prompt-injection-induced secret exfiltration. The shim workflow additions correctly route pull_request_review events and /fix commands with appropriate ACLs and fork-blocking. The test changes to use len(managedFiles) instead of hardcoded counts are a welcome improvement. No critical or high findings; several medium and low observations are documented below.

Findings

Medium

  • [Correctness] templates/shim-workflow.yaml dispatch-fix-bot condition — The bot login check format('{0}-review[bot]', github.repository_owner) assumes the review GitHub App is named {org}-review. If the App has a different naming convention, this condition will never match and the bot-triggered fix loop will silently not fire. This fails safe (no security risk) but could cause operational confusion.
    Remediation: Document the expected App naming convention prominently, or consider making the reviewer bot login configurable via a repository variable (e.g., vars.FULLSEND_REVIEW_BOT_LOGIN).

  • [Correctness] .github/workflows/fix.yml concurrency group — If event_payload contains neither .pull_request.number nor .issue.number AND inputs.pr_number is empty (allowed since required: false), the concurrency group resolves to fullsend-fix-, a shared group that could cause unrelated manual-dispatch runs to cancel each other. The validation step at line 222 catches this, but the concurrency group evaluates before the job body runs.
    Remediation: Consider making pr_number required for workflow_dispatch, or add a fallback value like github.run_id to ensure uniqueness.

  • [Style/conventions] .github/workflows/fix.yml — The dispatch-fix-bot and dispatch-fix-human jobs in the shim workflow use separate concurrency groups (fix-${{ github.event.pull_request.number }} vs fix-${{ github.event.issue.number }}). Per issue #197's acceptance criteria, human /fix commands should preempt bot runs. Since these are separate concurrency groups (one keyed on pull_request.number, the other on issue.number), a human /fix will NOT cancel a running bot-triggered fix on the same PR. The cross-cancellation only happens in fix.yml itself (single fullsend-fix-{pr} group), which means both dispatches will be sent and the second will cancel the first at the .fullsend workflow level. This works but relies on a timing race rather than explicit preemption at the shim level.
    Remediation: Consider using a unified concurrency group in the shim (e.g., fix-${{ github.event.pull_request.number || github.event.issue.number }}) for both dispatch jobs, so the human dispatch cancels the bot dispatch before it even reaches .fullsend.

Low

  • [Correctness] templates/shim-workflow.yaml dispatch-review condition — The new condition allows pull_request_review events with state commented (not changes_requested) from non-bot users to trigger the review agent. This means a human leaving a review comment (without approving or requesting changes) will re-trigger a full review run. This may be intentional but is not documented in the PR or code comments.

  • [Injection defense] .github/workflows/fix.yml instruction extraction — The random heredoc delimiter (openssl rand -hex 8) is a solid defense against GITHUB_OUTPUT injection. Minor note: echo "${INSTRUCTION}" is safe in bash but printf '%s\n' "${INSTRUCTION}" would be marginally more robust against edge cases with backslash-containing instructions.

  • [Correctness] scripts/post-fix.sh gitleaks scan of fix-result.json — The scan uses --no-git mode on a temp copy, which is correct. Note that gitleaks only catches known secret patterns; it won't catch arbitrary sensitive data the agent might embed via prompt injection. This is an inherent limitation, not a flaw.

  • [Style/conventions] scripts/post-fix.sh — Uses plain git push instead of --force-with-lease (unlike post-code.sh). The code comment explains this is intentional and tracked in #411. The plain push is arguably more conservative since it refuses diverged branches rather than force-overwriting.

Info

  • [Correctness] internal/layers/workflows_test.go — Replacing hardcoded counts (36, 37) with len(managedFiles) is a good improvement that prevents these tests from breaking every time a scaffold file is added.

  • [Correctness] Token isolation is correctly enforced: the sandbox receives only a read-only GH_TOKEN (contents:read, issues:read, pull-requests:read, metadata:read), while PUSH_TOKEN is confined to runner_env and never enters the sandbox environment.

  • [Correctness] The pre-fix.sh iteration cap check, instruction length cap (10KB), and input validation are well-implemented and match the acceptance criteria from issue #197.

Footer

Outcome: comment-only
This review applies to SHA b690f3bfe2139f70e8352d3fbbb6e0c286d25dea. Any push to the PR head clears this review and requires a new evaluation.

ascerra added a commit to ascerra/fullsend that referenced this pull request Apr 24, 2026
1. Use random heredoc delimiter for GITHUB_OUTPUT instruction to prevent
   delimiter injection from /fix comment body.
2. Fix Pushed status line to correctly print "yes"/"no" instead of
   printing "false" on success.
3. Switch process-fix-result.py from --body to --body-file - (stdin)
   to avoid exposing comment content in /proc/pid/cmdline.
4. Switch post-fix.sh from ls -t to glob-order loop for finding
   fix-result.json, matching post-triage.sh pattern.
5. Add comments documenting intentional design choices:
   - changes_requested exclusion from review dispatch (shim-workflow)
   - fullsend-fix author name with fullsend-code email (fix-agent.env)
   - plain git push without --force-with-lease (post-fix.sh)

HUMAN_INSTRUCTION expand:true risk deferred to fullsend-ai#408.

Made-with: Cursor

@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.

Review: #337

Head SHA: eb16265
Timestamp: 2026-04-24T00:00:00Z
Outcome: comment-only

Summary

This PR introduces the fix agent for automated PR review remediation, implementing Issue #197's Option A (separate workflow). The implementation is well-structured with strong security layering: read-only sandbox tokens, protected-path enforcement in both the agent definition and post-script, gitleaks scanning of agent output before posting, fork PR blocking, and a randomized heredoc delimiter to prevent GITHUB_OUTPUT injection from crafted /fix comments. The code follows existing patterns closely. Three medium findings are worth noting but none are blocking.

Findings

Medium

  • [missing-feature] scripts/process-fix-result.py — The strategy_change field defined in fix-result.schema.json is never rendered by build_summary_body(). When the fix agent changes strategy after hitting the escalation threshold, this information is captured in the JSON but silently dropped from the PR summary comment. Given the team sync's emphasis on decision-point observability, this field should be surfaced in the comment (e.g., as a callout before the actions list).

  • [scope-creep] templates/shim-workflow.yaml — The dispatch-review condition now includes pull_request_review events where state != 'changes_requested' and the author is not a bot. This means the review agent is re-triggered whenever a human submits an "approved" or "commented" review. This could cause unnecessary review cycles on busy PRs with multiple human reviewers, and the behavior is not discussed in Issue #197. Consider whether human review submissions should trigger the review agent, or whether this trigger should be limited to pull_request_target events (which already cover new pushes).

  • [missing-validation] harness/fix.yaml — Unlike the review agent's harness (which has a validation_loop for schema validation), the fix agent harness has no validation_loop for fix-result.schema.json. The skill instructs the agent to self-validate, but the review agent had the same instruction and still got a harness-level validation loop as a safety net. Consider adding one for consistency.

Low

  • [hardcoded-config] templates/shim-workflow.yaml:153 — The bot-triggered fix dispatch checks github.event.review.user.login == format('{0}-review[bot]', github.repository_owner), hardcoding the reviewer bot login convention. Issue #197 specifies a configurable reviewer whitelist via .fullsend/config.yaml. The hardcoded approach is more secure (not user-overridable) but less flexible. Acceptable for MVP if tracked as follow-up.

Info

  • [test-improvement] internal/layers/workflows_test.go — Replacing hardcoded file counts (36, 37) with len(managedFiles)-1 and len(managedFiles) is a good improvement that prevents these tests from breaking every time a scaffold file is added.

  • [security-positive] Multiple files — Defense-in-depth is well-layered: sandbox gets read-only token (contents:read, issues:read, pull-requests:read); push token stays on the runner; disallowedTools blocks git push, gh api, and PR mutations; post-script checks protected paths, runs gitleaks, and validates branch name; fork PRs are blocked at both the shim and workflow levels; HUMAN_INSTRUCTION uses a randomized heredoc delimiter to prevent output injection.

Footer

Outcome: comment-only
This review applies to SHA eb16265578355fcb8601f0ba97c8e0db55a356ef. Any push to the PR head clears this review and requires a new evaluation.

- Move inline ${{ }} PR_URL to env: block for consistent env-passing
- Build minimal dispatch payloads for both fix jobs (bot + human)
  to avoid exceeding GitHub's 65KB workflow_dispatch input limit
- Add minItems: 1 to actions array in fix-result.schema.json
- Render strategy_change field in process-fix-result.py summary
- Add allOf/if/then conditional schema tests (fix missing description,
  disagree missing reason, empty actions rejected)

Tested on ascerra-fullsend-lab:
  Run 25085229338 — all steps passed, agent posted summary on PR fullsend-ai#3

Made-with: Cursor

@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.

Review: #337

Head SHA: f4e378e
Timestamp: 2026-04-29T01:02:22Z
Outcome: approve

Summary

This PR adds the fix agent — the fourth agent in the fullsend pipeline — with comprehensive security layering that closely follows established patterns from the code and review agents. The change is well-scoped to issue #197, introduces no critical or high findings, and demonstrates careful attention to defense-in-depth (dual-token isolation, protected-path enforcement, authoritative secret scanning, fork PR blocking, iteration caps). The one medium-severity concern (HUMAN_INSTRUCTION passing through expand: true) is already tracked in #408.

Findings

Medium

  • [Content security] env/fix-agent.env + harness/fix.yamlHUMAN_INSTRUCTION is injected into fix-agent.env which uses expand: true, meaning os.ExpandEnv() runs on user-provided /fix instruction text. If a user writes /fix check ${PUSH_TOKEN}, the runner's environment variable would be expanded and passed into the sandbox. This is already tracked in #408 and explicitly documented in the PR body, so it is not blocking. The workflow's 10KB instruction length cap and the sandbox's read-only token scope limit the blast radius.
    Remediation: Already tracked in #408. The fix is to use a file-based passthrough (write instruction to a file, mount via host_files without expand: true) rather than embedding it in an expanded env file.

Low

  • [Correctness] scripts/post-fix.sh:90-108 — When pre-commit installation fails (pip, pip3, and pipx all fail), the script warns but continues, skipping authoritative pre-commit checks. This matches the code agent's post-script pattern and is acceptable for robustness, but means repos with .pre-commit-config.yaml on runners without Python could skip linting enforcement. The secret scan (gitleaks) is the harder gate and is not skippable.

  • [Style/conventions] scripts/process-fix-result-test.py:9sys.path.insert(0, os.path.dirname(__file__)) is redundant since the module is loaded via spec_from_file_location with an absolute path on the next lines. Not a bug.

Info

  • [Intent alignment] The PR scope matches issue #197's acceptance criteria and the team's confirmed decision (Option A: separate agent). All 10 scaffold files are registered in the e2e test assertions, managedFiles is used dynamically in workflow tests (replacing hardcoded counts), and the shim workflow correctly routes pull_request_review events to the fix agent while excluding the review agent from re-triggering on reviews.

  • [Correctness] fix.yml env var naming FIX_FIX_ITERATION is correct — setup-agent-env.sh strips the FIX_ agent prefix, leaving FIX_ITERATION as the sandbox variable name. The doubled prefix is consistent with other prefixed vars (FIX_GH_TOKENGH_TOKEN, FIX_PR_NUMBERPR_NUMBER).

  • [Platform security] Review body fetch in fix.yml correctly filters to the exact review bot login (${ORG_NAME}-review[bot]) and CHANGES_REQUESTED state, preventing injection via racing reviews from other users. The 1MB review body cap and gitleaks scan of fix-result.json before posting provide defense-in-depth against exfiltration.

  • [Injection defense] The shim workflow's dispatch-fix-human job passes COMMENT_BODY via env: block (not inline ${{ }}) and builds the payload with jq --arg (proper escaping). The fix.yml instruction extraction uses a random heredoc delimiter (openssl rand -hex 8) to prevent GITHUB_OUTPUT injection. No prompt injection patterns detected in PR body, commit messages, or code comments.

  • [Platform security] The fix agent's disallowedTools list correctly blocks gh api *, gh pr create/edit/merge *, gh issue edit/comment *, git push *, git add -A/./--all *, git commit --amend *, git reset --hard *, and git rebase *. The post-script enforces protected-path checks on agent commits before pushing.

Footer

Outcome: approve
This review applies to SHA f4e378ed23fcba3c4d4c2701dd34f2751e61baf0. Any push to the PR head clears this review and requires a new evaluation.

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thorough, well-structured PR that closes the review→fix loop. The implementation follows established patterns (dual-token isolation, harness pipeline, structured output schema), the security model is sound (fork blocking, protected-path checks, gitleaks scan, review-bot identity filtering), and the test coverage is adequate.

Already through 5 rounds of review with 3 reviewers — the derivative issues (#408, #411, #412, #434, #449, #450) show good discipline in deferring items that are out of scope rather than blocking the PR.

No outstanding concerns.

Bot-triggered review→fix loops are capped at 5 iterations. When the
cap is reached, the needs-human label is added. Human-triggered /fix
commands use a higher cap of 10, ensuring humans are never locked out
of the agent after a bot loop exhausts its budget.

- pre-fix.sh: select cap based on TRIGGER_SOURCE
- post-fix.sh: needs-human label only on bot-triggered runs
- fix-agent.env: add ITERATION_CAP_HUMAN=10
- agents/fix.md: updated iteration awareness docs

Tested on ascerra-fullsend-lab:
  Run 25113328340 — bot at iter 5: passed + needs-human label added
  Run 25113706269 — human at iter 6 of 10: passed (agent ran)
  Run 25114007809 — bot at iter 7 > 5: blocked by pre-fix

Made-with: Cursor
@fullsend-ai-review

fullsend-ai-review Bot commented Apr 29, 2026

Copy link
Copy Markdown

Review: #337

Head SHA: 730e138
Timestamp: 2026-04-29T00:00:00Z
Outcome: comment-only

Summary

This PR adds the fix agent — the fourth agent in the fullsend pipeline — implementing the review→fix automated loop. The implementation is well-structured, follows established patterns closely (harness, policies, env files, post-scripts), and demonstrates strong security awareness with dual-token isolation, protected-path enforcement, fork PR blocking, review body size caps, secret scanning of structured output, and randomized heredoc delimiters. The code is production-ready with thorough test coverage. A few medium and low findings are noted below for consideration but none are blocking.

Findings

Medium

  • [correctness] post-fix.sh:62 — Protected-path check uses bash prefix matching (${file} == ${pattern}*) without quoting the glob pattern. While this works for the current PROTECTED_PATHS values (simple directory prefixes and filenames), a path containing glob metacharacters ([, ?, *) would be interpreted as a glob rather than a literal prefix. The existing post-review.sh uses "${pattern}"* with the pattern quoted. Consider quoting for consistency: [[ "${file}" == "${pattern}"* ]].
    Remediation: Change if [[ "${file}" == ${pattern}* ]] to if [[ "${file}" == "${pattern}"* ]] to match post-review.sh.

  • [correctness] fix.yml:293 — The iteration counting query uses --paginate with --jq on the commits endpoint. If a PR has many commits, the jq filter [.[] | select(...)] | length runs per-page (each page is a separate JSON array), so the final result is the count from the last page only, not the total. For most PRs this won't matter (< 30 fix commits), but it's a latent correctness issue.
    Remediation: Pipe through jq -s 'add | length' after --paginate instead of using --jq inline, or accept the risk given the low iteration caps (5/10).

  • [missing-test] process-fix-result-test.py — No test covers the strategy_change value being null (JSON null) vs absent vs empty string. The schema allows "type": ["string", "null"] — verify build_summary_body handles null correctly (currently data.get("strategy_change", "") would get None for an explicit null, and if strategy_change: would correctly skip it, but this is worth an explicit test).
    Remediation: Add a test case with "strategy_change": null to confirm the None value is handled.

Low

  • [style] fix-agent.env:16 — Comment says "The email stays fullsend-code@ because both agents share the same GitHub App identity" which is a reasonable design choice, but could cause confusion when investigating commit attribution. The PR body mentions issue Fix agent: evaluate +fix email subaddressing for commit attribution #450 tracking +fix email subaddressing — this is adequately tracked.

  • [correctness] post-fix.sh:42 — The DIFF_BASE fallback uses git rev-parse HEAD~1 when PRE_AGENT_HEAD is unset. If the agent made zero commits, HEAD~1 would be the commit before the PR head — potentially scoping the diff too broadly. The code handles this gracefully (the CHANGED_FILES empty check on line 45 catches it), so the impact is minimal.

  • [style] fix-result.schema.json — The actions array has maxItems: 50 and files_changed has maxItems: 100. These limits seem reasonable but are not documented in the skill's structured output section. Consider adding a note about the limits in SKILL.md step 9 so the agent doesn't hit silent validation failures.

Info

  • [intent-alignment] The PR description says "Reverted stale auto-triage expansion (aligned with Disable automatic triage on issue open/edit — require /triage command #394 on main)" but the diff does not show any triage-related reverts. This appears to have been addressed in a prior revision — the current diff is clean.

  • [style] workflows_test.go — Good improvement replacing hardcoded counts with len(managedFiles). This makes the tests self-maintaining as new agent files are added.

  • [platform-security] The dual-token isolation pattern (read-only in sandbox, write-only in runner post-script) is correctly implemented and matches the established code/review agent pattern. The fix agent reuses the coder app credentials rather than requiring a new GitHub App — this is a pragmatic choice that reduces operational complexity.

  • [injection-defense] The randomized heredoc delimiter for HUMAN_INSTRUCTION in fix.yml:279 (INSTRUCTION_$(openssl rand -hex 8)) is a good defense against delimiter injection from /fix comment bodies. The review body is pre-fetched and filtered to exact bot login match, preventing injection via racing CHANGES_REQUESTED reviews.

Footer

Outcome: comment-only
This review applies to SHA 730e13831f5b3deac6120f8e878659fbca3f588a. Any push to the PR head clears this review and requires a new evaluation.

Previous run

Review: #337

Head SHA: 4709c72
Timestamp: 2026-04-29T14:24:27Z
Outcome: approve

Summary

This PR adds the fix agent — the fourth pipeline stage that closes the review→fix loop. The implementation is well-structured, follows established patterns from the code/review/triage agents, and demonstrates strong defense-in-depth security design. Token isolation, fork blocking, protected-path enforcement, secret scanning of structured output, review body filtering by exact bot login, and random heredoc delimiters for GITHUB_OUTPUT injection defense are all correctly implemented. Test coverage is thorough across unit tests, schema validation, and e2e assertions. Two minor observations are noted below but neither blocks approval.

Findings

Medium

  • [correctness] agents/fix.md vs scripts/post-fix.sh — The agent definition’s constraints text lists schemas/ and env/ as protected paths the agent cannot modify, but post-fix.sh’s PROTECTED_PATHS array does not include them. If the agent were to modify files in those directories (bypassing its own prompt-level constraint), the post-script would not block the push. This is a net improvement over post-code.sh which has no protected-path check at all, but the mismatch between documented constraints and enforcement could be tightened. Consider adding schemas/ and env/ to the PROTECTED_PATHS array in post-fix.sh for consistency with the agent definition.

Info

  • [style] harness/fix.yaml — The harness does not include a validation_loop max_iterations comment explaining why 2 was chosen (the review agent harness has the same pattern). Minor — the value is reasonable and matches the review agent.

  • [info] scripts/post-fix.sh — The HUMAN_INSTRUCTION env var passes through expand: true host_files, which could allow os.ExpandEnv to expand environment variable references in user-controlled input. Issue Fix agent: HUMAN_INSTRUCTION should not pass through bash expand #408 is already filed to track this. The 10KB length cap and random heredoc delimiter mitigate the practical risk.

Footer

Outcome: approve
This review applies to SHA 4709c72289759e6ee155fd21e2c8cc235c14eb93. Any push to the PR head clears this review and requires a new evaluation.

@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 above for full details.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Round 5 review — all 5 prior findings resolved. Two new documentation/behavior consistency findings below.

fix strategy.

Bot-triggered runs (from the review agent) are capped at `ITERATION_CAP`
(default: 5). When this cap is reached, the `needs-human` label is added and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[R5-L05] [MEDIUM] needs-human label timing: docs say "cap is reached" but code applies at cap−1

This line says "When this cap is reached, the needs-human label is added" — implying the label is applied when ITERATION == ITERATION_CAP (i.e., at iteration 5 with default cap 5).

However, post-fix.sh:247 sets WARN_THRESHOLD=$(( BOT_CAP - 1 )) and the condition on line 252 is ITERATION >= WARN_THRESHOLD, so the label is actually applied at iteration 4 (one before the cap). The same "cap is reached" wording in fix-agent.env:34 has the same mismatch.

The code behavior (warn before hitting the wall) is the better design — the docs should say "approaching" rather than "reached."

Suggested fix:

Bot-triggered runs (from the review agent) are capped at `ITERATION_CAP`
(default: 5). When the iteration count approaches this cap, the `needs-human`
label is added and the autonomous loop stops.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 730e138 — updated agents/fix.md and fix-agent.env to say "approaches" instead of "reached." Also clarified that the total is bot+human combined.

if [ "${NO_PUSH}" = "true" ]; then echo " Pushed: no"; else echo " Pushed: yes"; fi
echo " Trigger: ${TRIGGER_SOURCE}"
if [ "${TRIGGER_SOURCE:-bot}" = "human" ]; then
echo " Iteration: ${ITERATION} of ${ITERATION_CAP_HUMAN:-10} (human cap)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[R5-W05] [MEDIUM] "of 10 (human cap)" implies 10 dedicated human iterations but the counter is shared

This log line displays Iteration: N of 10 (human cap), which reads as "you have 10 human iterations." In reality, FIX_ITERATION counts all fix-agent commits (bot + human) because fix.yml counts commits by author name fullsend-fix regardless of trigger source. After 4 bot iterations + 6 human /fix runs = 10 total, even /fix is blocked.

The pre-fix.sh error message (line 78) correctly says "total iterations" but this summary line does not.

Suggested fix:

echo "  Iteration: ${ITERATION} of ${ITERATION_CAP_HUMAN:-10} (human cap, total across bot+human)"

Alternately, fix.md:142 already says "total iterations" — consider matching that wording here for consistency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 730e138 — the summary line now reads:

Iteration: 6 of 10 (human cap, total across bot+human)

Matches the "total iterations" wording in pre-fix.sh line 78 and fix.md.

- agents/fix.md: "approaching" not "reached" (label fires at cap-1)
- fix-agent.env: same wording fix
- post-fix.sh: clarify human cap log is "total across bot+human"

Made-with: Cursor

@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 above for full details.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM — all findings from rounds 1–6 resolved.

Round 6 summary (4-agent final pass):

  • R5-L05 (iteration cap docs): resolved — "approaches this cap" matches BOT_CAP - 1 behavior
  • R5-W05 (human cap ambiguity): resolved — all 3 locations now clarify shared counter (bot + human combined)
  • No new findings across security, logic, workflow, or test coverage reviews

Cumulative resolved findings (rounds 1–6):

  1. W1: script injection via inline ${{ }} — moved to env blocks
  2. W2: 65KB dispatch payload limit — minimal jq payloads
  3. L03: empty actions array accepted by schema — fixed with minItems: 1
  4. TC-01: no tests for conditional schema rules — 5 tests added
  5. TC-02: strategy_change not rendered — now rendered and tested
  6. R5-L05: iteration cap docs/code mismatch — wording aligned
  7. R5-W05: human cap log ambiguity — clarified shared counter

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.

Story 9: Fix Agent

4 participants