Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/agent-fix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Agent Auto-Fix

on:
issues:
types: [opened, labeled]

jobs:
agent-fix:
# Run only if the issue is labeled with 'agent-fix', or opened with 'agent-fix' label.
if: github.event.label.name == 'agent-fix' || (github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'agent-fix'))
runs-on: ubuntu-latest

permissions:
contents: write
pull-requests: write
issues: write

steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Run Developer Agent
id: run-agent
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AGENT_TASK: |
Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }}

${{ github.event.issue.body }}
run: |
python main.py \
--repo . \
--push \
--pr \
--base-branch ${{ github.event.repository.default_branch }} \
--yes

- name: Post Comment on Issue
if: always()
uses: actions/github-script@v7
with:
script: |
const issueNumber = context.issue.number;
const outcome = "${{ steps.run-agent.outputs.outcome }}";
const prUrl = "${{ steps.run-agent.outputs.pr_url }}";
const runId = "${{ steps.run-agent.outputs.run_id }}";
const message = "${{ steps.run-agent.outputs.final_message }}";
Comment on lines +63 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n '\$\{\{\s*steps\.run-agent\.outputs\.' .github/workflows/agent-fix.yml

Repository: sreerevanth/repopilot

Length of output: 451


🏁 Script executed:

#!/bin/bash
sed -n '1,220p' .github/workflows/agent-fix.yml

Repository: sreerevanth/repopilot

Length of output: 3135


🏁 Script executed:

#!/bin/bash
rg -n "final_message|pr_url|run_id|outcome" -S .

Repository: sreerevanth/repopilot

Length of output: 7332


🏁 Script executed:

#!/bin/bash
sed -n '150,230p' main.py

Repository: sreerevanth/repopilot

Length of output: 2220


🏁 Script executed:

#!/bin/bash
rg -n "def write_github_output|write_github_output\(" -S .

Repository: sreerevanth/repopilot

Length of output: 355


🏁 Script executed:

#!/bin/bash
sed -n '90,120p' main.py

Repository: sreerevanth/repopilot

Length of output: 1216


🏁 Script executed:

#!/bin/bash
sed -n '396,414p' modules/agent_loop.py

Repository: sreerevanth/repopilot

Length of output: 1096


Quote the step outputs before interpolating them into github-script. Raw substitution is evaluated as JS here; final_message can carry arbitrary text on the error path, so quotes/newlines can break the script and turn into code execution. Use toJSON() for all four outputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/agent-fix.yml around lines 63 - 66, The github-script
interpolation in the agent-fix workflow is using raw step outputs, which can
break the script or become unsafe when the output contains quotes/newlines.
Update the script block that reads the run-agent outputs (outcome, pr_url,
run_id, final_message) to serialize each value with toJSON() before assigning to
variables, and ensure the existing github-script snippet still uses those
symbols safely.

const issuee = context.payload.issue.user.login;

let commentBody = "";
if (outcome === "success") {
commentBody = `Hello @${issuee},

I have successfully fixed the issue! 🎉

A Pull Request has been created with the changes:
🔗 **[Pull Request](${prUrl})**

**Run ID:** \`${runId}\`
**Message:** ${message}`;
Comment on lines +70 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not claim a PR was created when pr_url is empty.

outcome === "success" only means the agent completed/tests passed; pr_url can still be empty if push or PR creation fails. Gate this branch on prUrl too.

Proposed fix
-            if (outcome === "success") {
+            if (outcome === "success" && prUrl) {
               commentBody = `Hello @${issuee},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (outcome === "success") {
commentBody = `Hello @${issuee},
I have successfully fixed the issue! 🎉
A Pull Request has been created with the changes:
🔗 **[Pull Request](${prUrl})**
**Run ID:** \`${runId}\`
**Message:** ${message}`;
if (outcome === "success" && prUrl) {
commentBody = `Hello @${issuee},
I have successfully fixed the issue! 🎉
A Pull Request has been created with the changes:
🔗 **[Pull Request](${prUrl})**
**Run ID:** \`${runId}\`
**Message:** ${message}`;
🧰 Tools
🪛 actionlint (1.7.12)

[error] 73-73: could not parse as YAML: could not find expected ':'

(syntax-check)

🪛 YAMLlint (1.37.1)

[error] 75-75: syntax error: could not find expected ':'

(syntax)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/agent-fix.yml around lines 70 - 79, The success comment in
the workflow still claims a Pull Request was created even when prUrl is empty.
Update the success branch in agent-fix.yml so the message that includes the Pull
Request link only runs when both outcome === "success" and prUrl is present;
otherwise fall back to a message that does not mention a PR. Use the existing
outcome, prUrl, commentBody, and runId variables to locate and gate the logic.

} else {
commentBody = `Hello @${issuee},

I attempted to fix this issue automatically but encountered a problem.

**Outcome:** \`${outcome || 'unknown/failed'}\`
**Message:** ${message || 'No additional message was provided.'}

Please check the GitHub Actions workflow logs for more details.`;
Comment on lines +71 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
if command -v actionlint >/dev/null; then
  actionlint .github/workflows/agent-fix.yml
fi
if command -v yamllint >/dev/null; then
  yamllint .github/workflows/agent-fix.yml
fi

Repository: sreerevanth/repopilot

Length of output: 854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant workflow section with line numbers.
sed -n '55,100p' .github/workflows/agent-fix.yml | cat -n

# Also show the surrounding job/step structure to confirm the YAML nesting.
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/agent-fix.yml')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 50 <= i <= 105:
        print(f"{i:4}: {line}")
PY

Repository: sreerevanth/repopilot

Length of output: 3701


Indent the template literal body inside script: |
The unindented lines in both multi-line strings break the YAML block and prevent the workflow from parsing.

🧰 Tools
🪛 actionlint (1.7.12)

[error] 73-73: could not parse as YAML: could not find expected ':'

(syntax-check)

🪛 YAMLlint (1.37.1)

[error] 75-75: syntax error: could not find expected ':'

(syntax)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/agent-fix.yml around lines 71 - 88, The multi-line
template literals assigned to commentBody inside the workflow’s script block are
not indented correctly for YAML, causing the block scalar to break. Update the
strings in the agent-fix workflow so the lines within each backtick template
remain consistently indented under script: |, and verify both the success and
failure branches in the commentBody assignment preserve valid YAML parsing.

Source: Linters/SAST tools

}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: commentBody
});
59 changes: 55 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
"""

import argparse
from html import parser
import os
import sys

# Ensure the project root is on the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from modules.agent_loop import AutonomousAgent, AgentConfig
from modules.code_modifier import CodeModificationEngine


def parse_args() -> argparse.Namespace:
Expand All @@ -41,7 +41,7 @@ def parse_args() -> argparse.Namespace:
)

parser.add_argument("--repo", required=True, help="Path to the git repository")
parser.add_argument("--task", required=True, help="High-level task description")
parser.add_argument("--task", required=False, help="High-level task description (can also be provided via AGENT_TASK env var)")

# Execution
parser.add_argument("--runner", default="pytest",
Expand Down Expand Up @@ -91,26 +91,52 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--api-key", default=None,
help="Anthropic API key (default: ANTHROPIC_API_KEY env var)")

# Non-interactive / CI
parser.add_argument("--yes", "-y", action="store_true",
help="Auto-approve file changes (bypass confirmation prompt)")

return parser.parse_args()


def write_github_output(outputs: dict[str, str]):
github_output_path = os.environ.get("GITHUB_OUTPUT")
if github_output_path:
try:
with open(github_output_path, "a", encoding="utf-8") as f:
for k, v in outputs.items():
if "\n" in v:
delimiter = "EOF"
f.write(f"{k}<<{delimiter}\n{v}\n{delimiter}\n")
else:
f.write(f"{k}={v}\n")
except Exception as e:
print(f"Failed to write to GITHUB_OUTPUT: {e}", file=sys.stderr)


def main():
args = parse_args()

repo_root = os.path.abspath(args.repo)
if args.rollback:
modifier = CodeModificationEngine(repo_root=repo_root, backup_dir="backups")
modifier = CodeModificationEngine(backup_dir="backups")
success = modifier.git_stash_pop(repo_root)
sys.exit(0 if success else 1)
if not os.path.isdir(repo_root):
print(f"ERROR: Repository path does not exist: {repo_root}", file=sys.stderr)
sys.exit(1)

yes_flag = args.yes or os.environ.get("CI") == "true"

task = args.task or os.environ.get("AGENT_TASK")
if not task:
print("ERROR: Task description is required. Provide --task or set the AGENT_TASK environment variable.", file=sys.stderr)
sys.exit(1)
Comment on lines 124 to +133

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Write GitHub outputs for pre-run validation failures.

These sys.exit(1) paths run before the try block, so CI consumers get empty steps.run-agent.outputs.* instead of outcome=error.

Proposed fix
     if not os.path.isdir(repo_root):
-        print(f"ERROR: Repository path does not exist: {repo_root}", file=sys.stderr)
+        message = f"Repository path does not exist: {repo_root}"
+        print(f"ERROR: {message}", file=sys.stderr)
+        write_github_output({
+            "outcome": "error",
+            "run_id": "",
+            "iterations": "0",
+            "branch_name": "",
+            "pr_url": "",
+            "final_message": message,
+        })
         sys.exit(1)
 
     yes_flag = args.yes or os.environ.get("CI") == "true"
 
     task = args.task or os.environ.get("AGENT_TASK")
     if not task:
-        print("ERROR: Task description is required. Provide --task or set the AGENT_TASK environment variable.", file=sys.stderr)
+        message = "Task description is required. Provide --task or set the AGENT_TASK environment variable."
+        print(f"ERROR: {message}", file=sys.stderr)
+        write_github_output({
+            "outcome": "error",
+            "run_id": "",
+            "iterations": "0",
+            "branch_name": "",
+            "pr_url": "",
+            "final_message": message,
+        })
         sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not os.path.isdir(repo_root):
print(f"ERROR: Repository path does not exist: {repo_root}", file=sys.stderr)
sys.exit(1)
yes_flag = args.yes or os.environ.get("CI") == "true"
task = args.task or os.environ.get("AGENT_TASK")
if not task:
print("ERROR: Task description is required. Provide --task or set the AGENT_TASK environment variable.", file=sys.stderr)
sys.exit(1)
if not os.path.isdir(repo_root):
message = f"Repository path does not exist: {repo_root}"
print(f"ERROR: {message}", file=sys.stderr)
write_github_output({
"outcome": "error",
"run_id": "",
"iterations": "0",
"branch_name": "",
"pr_url": "",
"final_message": message,
})
sys.exit(1)
yes_flag = args.yes or os.environ.get("CI") == "true"
task = args.task or os.environ.get("AGENT_TASK")
if not task:
message = "Task description is required. Provide --task or set the AGENT_TASK environment variable."
print(f"ERROR: {message}", file=sys.stderr)
write_github_output({
"outcome": "error",
"run_id": "",
"iterations": "0",
"branch_name": "",
"pr_url": "",
"final_message": message,
})
sys.exit(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` around lines 124 - 133, The pre-run validation exits in main()
currently call sys.exit(1) before the try block, so GitHub Actions never
receives failure outputs. Update the repo_root and task validation paths in
main.py to write the expected GitHub outputs before exiting, using the same
output-writing mechanism already used for run-agent outcomes so CI consumers see
an error state instead of empty steps.run-agent.outputs.* values.


config = AgentConfig(
repo_root=repo_root,
task=args.task,
task=task,
dry_run=args.dry_run,
yes=yes_flag,

# Execution
test_runner=args.runner,
Expand Down Expand Up @@ -155,15 +181,40 @@ def main():
print(f"MESSAGE : {result.final_message}")
print(f"{'='*60}")

write_github_output({
"outcome": result.outcome,
"run_id": result.run_id,
"iterations": str(result.iterations_used),
"branch_name": result.branch_name or "",
"pr_url": result.pr_url or "",
"final_message": result.final_message,
})

sys.exit(0 if result.outcome == "success" else 1)

except KeyboardInterrupt:
print("\nInterrupted by user.", file=sys.stderr)
write_github_output({
"outcome": "aborted",
"run_id": "",
"iterations": "0",
"branch_name": "",
"pr_url": "",
"final_message": "Interrupted by user.",
})
sys.exit(130)
except Exception as e:
print(f"\nFATAL ERROR: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
write_github_output({
"outcome": "error",
"run_id": "",
"iterations": "0",
"branch_name": "",
"pr_url": "",
"final_message": str(e),
})
sys.exit(1)


Expand Down
3 changes: 2 additions & 1 deletion modules/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class AgentConfig:
repo_root: str
task: str
dry_run: bool = False
yes: bool = False

# Execution
test_runner: str = "pytest" # pytest | npm_test | go | cargo | ...
Expand Down Expand Up @@ -296,7 +297,7 @@ def run(self) -> AgentRunResult:
pr_url=None,
)

if not ask_confirmation(len(llm_resp.changes)):
if not (self.config.yes or ask_confirmation(len(llm_resp.changes))):
return AgentRunResult(
outcome="aborted",
run_id=self.run_id,
Expand Down