diff --git a/.github/workflows/agent-fix.yml b/.github/workflows/agent-fix.yml new file mode 100644 index 0000000..0ab192d --- /dev/null +++ b/.github/workflows/agent-fix.yml @@ -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 }}"; + 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}`; + } 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.`; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: commentBody + }); diff --git a/main.py b/main.py index 29330da..2ad959a 100644 --- a/main.py +++ b/main.py @@ -10,7 +10,6 @@ """ import argparse -from html import parser import os import sys @@ -18,6 +17,7 @@ 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: @@ -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", @@ -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) + config = AgentConfig( repo_root=repo_root, - task=args.task, + task=task, dry_run=args.dry_run, + yes=yes_flag, # Execution test_runner=args.runner, @@ -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) diff --git a/modules/agent_loop.py b/modules/agent_loop.py index a1fff7f..0db00c8 100644 --- a/modules/agent_loop.py +++ b/modules/agent_loop.py @@ -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 | ... @@ -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,