From 2df37b671b679d8770f621a22260a28fc3926404 Mon Sep 17 00:00:00 2001 From: Christian Meyer Date: Fri, 21 Aug 2026 23:46:16 +0200 Subject: [PATCH] Improve coding agent reliability --- .env.example | 2 +- README.md | 2 +- docker-compose.yml | 2 +- src/lai_automation/agent.py | 15 +++++++++++++++ src/lai_automation/config.py | 2 +- src/lai_automation/policy.py | 1 + src/lai_automation/prompt.py | 1 + src/lai_automation/worker.py | 3 ++- tests/test_agent.py | 3 ++- tests/test_policy.py | 7 +++++-- tests/test_prompt.py | 1 + 11 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 753fe27..ee4a771 100644 --- a/.env.example +++ b/.env.example @@ -17,7 +17,7 @@ AUTOMATION_MODE=dry-run ALLOW_DIRECT_MAIN=false AGENT_COMMAND=aider -AGENT_MODEL=mistral/mistral-medium-latest +AGENT_MODEL=mistral/devstral-latest MISTRAL_API_KEY= POLL_SECONDS=60 diff --git a/README.md b/README.md index 95747e9..c0b387a 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ die empfohlene Betriebsart. Default: ```dotenv -AGENT_MODEL=mistral/mistral-medium-latest +AGENT_MODEL=mistral/devstral-latest ``` Das Chat-Modell und Coding-Modell sind bewusst getrennt. Fuer Coding-Agenten sollte ein diff --git a/docker-compose.yml b/docker-compose.yml index 3e23f5d..b23c8e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: GITEA_USERNAME: ${GITEA_USERNAME:-automation-bot} GITEA_TOKEN: ${GITEA_TOKEN:-} AGENT_COMMAND: ${AGENT_COMMAND:-aider} - AGENT_MODEL: ${AGENT_MODEL:-mistral/mistral-medium-latest} + AGENT_MODEL: ${AGENT_MODEL:-mistral/devstral-latest} MISTRAL_API_KEY: ${MISTRAL_API_KEY:-} POLL_SECONDS: ${POLL_SECONDS:-60} STALE_CLAIM_MINUTES: ${STALE_CLAIM_MINUTES:-90} diff --git a/src/lai_automation/agent.py b/src/lai_automation/agent.py index d18b0dc..be04409 100644 --- a/src/lai_automation/agent.py +++ b/src/lai_automation/agent.py @@ -2,6 +2,7 @@ from __future__ import annotations import os import pwd +import shutil import subprocess from pathlib import Path @@ -18,6 +19,13 @@ class AiderAgent: task_file.chmod(0o600) command = self.command(task_file) coder = pwd.getpwnam("coder") + git_config = Path(coder.pw_dir) / ".gitconfig" + git_config.write_text( + "[user]\n\tname = LAI Automation Coder\n\temail = coder@localhost\n" + f"[safe]\n\tdirectory = {workspace.resolve()}\n" + ) + git_config.chmod(0o600) + os.chown(git_config, coder.pw_uid, coder.pw_gid) os.chown(task_file, coder.pw_uid, coder.pw_gid) for path in workspace.rglob("*"): if ".git" not in path.parts: @@ -49,6 +57,9 @@ class AiderAgent: timeout=900, ) finally: + shutil.rmtree(workspace / ".aider.tags.cache.v4", ignore_errors=True) + for artifact in (".aider.chat.history.md", ".aider.input.history"): + (workspace / artifact).unlink(missing_ok=True) os.chown(workspace, 0, coder.pw_gid, follow_symlinks=False) workspace.chmod(0o775) if result.returncode != 0: @@ -72,4 +83,8 @@ class AiderAgent: "--input-history-file", "/dev/null", "--no-analytics", + "--edit-format", + "diff", + "--timeout", + "60", ] diff --git a/src/lai_automation/config.py b/src/lai_automation/config.py index 59f7a76..a4e3888 100644 --- a/src/lai_automation/config.py +++ b/src/lai_automation/config.py @@ -64,7 +64,7 @@ class Settings: gitea_token=os.getenv("GITEA_TOKEN", ""), gitea_username=os.getenv("GITEA_USERNAME", "automation-bot"), agent_command=os.getenv("AGENT_COMMAND", "aider"), - agent_model=os.getenv("AGENT_MODEL", "mistral/mistral-medium-latest"), + agent_model=os.getenv("AGENT_MODEL", "mistral/devstral-latest"), test_commands=_commands( os.getenv( "TEST_COMMANDS", diff --git a/src/lai_automation/policy.py b/src/lai_automation/policy.py index 7e96b2b..9f107b7 100644 --- a/src/lai_automation/policy.py +++ b/src/lai_automation/policy.py @@ -15,6 +15,7 @@ _ALWAYS_DENIED = ( "docker-compose", ".gitea/", ".github/", + ".aider", ) _DIRECT_MAIN_DENIED = ( diff --git a/src/lai_automation/prompt.py b/src/lai_automation/prompt.py index 961e2b4..e318cdc 100644 --- a/src/lai_automation/prompt.py +++ b/src/lai_automation/prompt.py @@ -36,6 +36,7 @@ def build_task(report: FailureReport) -> str: Rules: - First add a regression test that reproduces the reported behavior. +- The regression test must execute the relevant production behavior; never mock or stub the behavior being tested. - Make the smallest production change that fixes the root cause. - Preserve read-only SQL validation and tenant isolation. - Never read or modify .env files, credentials, deployment secrets, or production data. diff --git a/src/lai_automation/worker.py b/src/lai_automation/worker.py index 4be56d1..0849ee1 100644 --- a/src/lai_automation/worker.py +++ b/src/lai_automation/worker.py @@ -108,7 +108,8 @@ class AutomationWorker: "stderr": result.stderr[-6000:], }) if result.returncode != 0: - raise RuntimeError(f"Test command failed: {command}\n{result.stderr[-2000:]}") + output = (result.stdout + "\n" + result.stderr)[-4000:] + raise RuntimeError(f"Test command failed: {command}\n{output}") return results @staticmethod diff --git a/tests/test_agent.py b/tests/test_agent.py index 2148658..cf9c7ee 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -11,4 +11,5 @@ def test_aider_command_cannot_mutate_git_configuration_or_send_analytics() -> No assert "--no-gitignore" in command assert "--no-analytics" in command assert "--no-auto-commits" in command - assert command.count("/dev/null") == 2 \ No newline at end of file + assert command.count("/dev/null") == 2 + assert command[-4:] == ["--edit-format", "diff", "--timeout", "60"] \ No newline at end of file diff --git a/tests/test_policy.py b/tests/test_policy.py index 9972bc5..b77f5e4 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -15,9 +15,12 @@ def test_policy_accepts_small_application_change() -> None: def test_policy_rejects_secrets_and_vendor_paths() -> None: - result = evaluate_changes(ChangeSummary((".env", "vendor/foo.php"), 2, 0), settings()) + result = evaluate_changes( + ChangeSummary((".env", "vendor/foo.php", ".aider.tags.cache.v4/cache.db"), 3, 0), + settings(), + ) assert not result.allowed - assert len(result.errors) == 2 + assert len(result.errors) == 3 def test_direct_main_blocks_validator_and_migrations() -> None: diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 265cde5..a700cdd 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -30,6 +30,7 @@ def test_task_contains_constraints_and_redacted_report() -> None: assert "secret-value" not in task assert "admin@example.com" not in task assert "Do not commit or push" in task + assert "never mock or stub the behavior being tested" in task def test_redact_removes_extended_credentials() -> None: