diff --git a/src/lai_automation/agent.py b/src/lai_automation/agent.py index 352b461..84bde6e 100644 --- a/src/lai_automation/agent.py +++ b/src/lai_automation/agent.py @@ -13,11 +13,11 @@ class AiderAgent: def __init__(self, settings: Settings) -> None: self.settings = settings - def run(self, workspace: Path, task: str) -> str: + def run(self, workspace: Path, task: str, files: tuple[str, ...] = ()) -> str: task_file = workspace / ".git" / "failure-task.txt" task_file.write_text(task) task_file.chmod(0o600) - command = self.command(task_file) + command = self.command(task_file, files) coder = pwd.getpwnam("coder") git_config = Path(coder.pw_dir) / ".gitconfig" git_config.write_text( @@ -66,8 +66,8 @@ class AiderAgent: raise RuntimeError(f"Aider failed ({result.returncode}): {result.stderr[-4000:]}") return result.stdout[-12_000:] - def command(self, task_file: Path) -> list[str]: - return [ + def command(self, task_file: Path, files: tuple[str, ...] = ()) -> list[str]: + command = [ self.settings.agent_command, "--model", self.settings.agent_model, @@ -88,3 +88,7 @@ class AiderAgent: "--timeout", "180", ] + for filename in files: + if (task_file.parent.parent / filename).is_file(): + command.extend(["--file", filename]) + return command diff --git a/src/lai_automation/policy.py b/src/lai_automation/policy.py index 909f449..95df5eb 100644 --- a/src/lai_automation/policy.py +++ b/src/lai_automation/policy.py @@ -71,7 +71,9 @@ def evaluate_changes( def evaluate_report_scope(report: FailureReport, summary: ChangeSummary) -> PolicyResult: expected = f"{report.user_feedback or ''}\n{report.expected_behavior or ''}".lower() - requires_cross_database_orchestration = "cockpit_config" in expected and "fk_" in expected + requires_cross_database_orchestration = ( + "cockpit_config" in expected and ("fk_" in expected or "erp" in expected) + ) has_application_change = any(filename.startswith("app/") for filename in summary.files) errors = [] if requires_cross_database_orchestration and not has_application_change: diff --git a/src/lai_automation/prompt.py b/src/lai_automation/prompt.py index 4d74891..fef7f8b 100644 --- a/src/lai_automation/prompt.py +++ b/src/lai_automation/prompt.py @@ -59,6 +59,23 @@ def redact(value: str | None) -> str: return result[:12_000] +def repair_files(report: FailureReport) -> tuple[str, ...]: + expected = f"{report.user_feedback or ''}\n{report.expected_behavior or ''}".lower() + if "cockpit_config" in expected and ("fk_" in expected or "erp" in expected): + return ( + "app/Http/Controllers/ChatController.php", + "app/Services/TenantDatabaseService.php", + "tests/Unit/ChatControllerSourceResolutionTest.php", + ) + if '__LAI_' in (report.response or ''): + return ( + "app/Services/AIService.php", + "app/Http/Controllers/ChatController.php", + "tests/Feature/AIPrivacyBoundaryTest.php", + ) + return () + + def build_task(report: FailureReport) -> str: prompt = redact(report.prompt) feedback = redact(report.user_feedback) diff --git a/src/lai_automation/worker.py b/src/lai_automation/worker.py index 521a33c..4048a8a 100644 --- a/src/lai_automation/worker.py +++ b/src/lai_automation/worker.py @@ -18,7 +18,7 @@ from .git import ChangeSummary, GitWorkspace from .gitea import GiteaClient from .models import FailureReport from .policy import evaluate_changes, evaluate_report_scope -from .prompt import build_retry_task, build_task +from .prompt import build_retry_task, build_task, repair_files LOG = logging.getLogger(__name__) @@ -57,6 +57,9 @@ class AutomationWorker: workspace.prepare() LOG.info("Report %s branch=%s workspace ready", report.id, workspace.branch) task = build_task(report) + files = repair_files(report) + if files: + LOG.info("Report %s routed editable files=%s", report.id, ",".join(files)) metadata["agent_output"] = [] valid_checkpoint: str | None = None for attempt in range(1, self.settings.max_agent_attempts + 1): @@ -67,7 +70,7 @@ class AutomationWorker: self.settings.max_agent_attempts, ) with self._progress(report.id, f"agent attempt {attempt}"): - metadata["agent_output"].append(self.agent.run(workspace.path, task)) + metadata["agent_output"].append(self.agent.run(workspace.path, task, files)) summary = workspace.summary() metadata["changes"] = asdict(summary) LOG.info( @@ -94,6 +97,8 @@ class AutomationWorker: attempt, "; ".join(policy.errors), ) + if not scope_policy.allowed: + workspace.restore_checkpoint(None) task = build_retry_task( report, RuntimeError( diff --git a/tests/test_agent.py b/tests/test_agent.py index 562be51..c613cc7 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -15,6 +15,22 @@ def test_aider_command_cannot_mutate_git_configuration_or_send_analytics() -> No assert command[-4:] == ["--edit-format", "udiff", "--timeout", "180"] +def test_aider_command_adds_existing_routed_files(tmp_path) -> None: + git_dir = tmp_path / ".git" + git_dir.mkdir() + controller = tmp_path / "app/Http/Controllers/ChatController.php" + controller.parent.mkdir(parents=True) + controller.write_text(" None: config = settings(workspace_root=tmp_path) workspace = tmp_path / "repo" diff --git a/tests/test_prompt.py b/tests/test_prompt.py index d06316f..41bdfda 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -1,5 +1,5 @@ from lai_automation.models import FailureReport -from lai_automation.prompt import build_retry_task, build_task, redact +from lai_automation.prompt import build_retry_task, build_task, redact, repair_files def test_redact_removes_common_secrets_and_personal_markers() -> None: @@ -200,3 +200,8 @@ def test_cross_database_report_guides_staged_source_routing() -> None: assert "staged-query workflows" in task assert "Do not put an impossible cross-connection join" in task assert "preserving tenant isolation" in task + assert repair_files(report) == ( + "app/Http/Controllers/ChatController.php", + "app/Services/TenantDatabaseService.php", + "tests/Unit/ChatControllerSourceResolutionTest.php", + )