This commit is contained in:
@@ -13,11 +13,11 @@ class AiderAgent:
|
|||||||
def __init__(self, settings: Settings) -> None:
|
def __init__(self, settings: Settings) -> None:
|
||||||
self.settings = settings
|
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 = workspace / ".git" / "failure-task.txt"
|
||||||
task_file.write_text(task)
|
task_file.write_text(task)
|
||||||
task_file.chmod(0o600)
|
task_file.chmod(0o600)
|
||||||
command = self.command(task_file)
|
command = self.command(task_file, files)
|
||||||
coder = pwd.getpwnam("coder")
|
coder = pwd.getpwnam("coder")
|
||||||
git_config = Path(coder.pw_dir) / ".gitconfig"
|
git_config = Path(coder.pw_dir) / ".gitconfig"
|
||||||
git_config.write_text(
|
git_config.write_text(
|
||||||
@@ -66,8 +66,8 @@ class AiderAgent:
|
|||||||
raise RuntimeError(f"Aider failed ({result.returncode}): {result.stderr[-4000:]}")
|
raise RuntimeError(f"Aider failed ({result.returncode}): {result.stderr[-4000:]}")
|
||||||
return result.stdout[-12_000:]
|
return result.stdout[-12_000:]
|
||||||
|
|
||||||
def command(self, task_file: Path) -> list[str]:
|
def command(self, task_file: Path, files: tuple[str, ...] = ()) -> list[str]:
|
||||||
return [
|
command = [
|
||||||
self.settings.agent_command,
|
self.settings.agent_command,
|
||||||
"--model",
|
"--model",
|
||||||
self.settings.agent_model,
|
self.settings.agent_model,
|
||||||
@@ -88,3 +88,7 @@ class AiderAgent:
|
|||||||
"--timeout",
|
"--timeout",
|
||||||
"180",
|
"180",
|
||||||
]
|
]
|
||||||
|
for filename in files:
|
||||||
|
if (task_file.parent.parent / filename).is_file():
|
||||||
|
command.extend(["--file", filename])
|
||||||
|
return command
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ def evaluate_changes(
|
|||||||
|
|
||||||
def evaluate_report_scope(report: FailureReport, summary: ChangeSummary) -> PolicyResult:
|
def evaluate_report_scope(report: FailureReport, summary: ChangeSummary) -> PolicyResult:
|
||||||
expected = f"{report.user_feedback or ''}\n{report.expected_behavior or ''}".lower()
|
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)
|
has_application_change = any(filename.startswith("app/") for filename in summary.files)
|
||||||
errors = []
|
errors = []
|
||||||
if requires_cross_database_orchestration and not has_application_change:
|
if requires_cross_database_orchestration and not has_application_change:
|
||||||
|
|||||||
@@ -59,6 +59,23 @@ def redact(value: str | None) -> str:
|
|||||||
return result[:12_000]
|
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:
|
def build_task(report: FailureReport) -> str:
|
||||||
prompt = redact(report.prompt)
|
prompt = redact(report.prompt)
|
||||||
feedback = redact(report.user_feedback)
|
feedback = redact(report.user_feedback)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from .git import ChangeSummary, GitWorkspace
|
|||||||
from .gitea import GiteaClient
|
from .gitea import GiteaClient
|
||||||
from .models import FailureReport
|
from .models import FailureReport
|
||||||
from .policy import evaluate_changes, evaluate_report_scope
|
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__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -57,6 +57,9 @@ class AutomationWorker:
|
|||||||
workspace.prepare()
|
workspace.prepare()
|
||||||
LOG.info("Report %s branch=%s workspace ready", report.id, workspace.branch)
|
LOG.info("Report %s branch=%s workspace ready", report.id, workspace.branch)
|
||||||
task = build_task(report)
|
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"] = []
|
metadata["agent_output"] = []
|
||||||
valid_checkpoint: str | None = None
|
valid_checkpoint: str | None = None
|
||||||
for attempt in range(1, self.settings.max_agent_attempts + 1):
|
for attempt in range(1, self.settings.max_agent_attempts + 1):
|
||||||
@@ -67,7 +70,7 @@ class AutomationWorker:
|
|||||||
self.settings.max_agent_attempts,
|
self.settings.max_agent_attempts,
|
||||||
)
|
)
|
||||||
with self._progress(report.id, f"agent attempt {attempt}"):
|
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()
|
summary = workspace.summary()
|
||||||
metadata["changes"] = asdict(summary)
|
metadata["changes"] = asdict(summary)
|
||||||
LOG.info(
|
LOG.info(
|
||||||
@@ -94,6 +97,8 @@ class AutomationWorker:
|
|||||||
attempt,
|
attempt,
|
||||||
"; ".join(policy.errors),
|
"; ".join(policy.errors),
|
||||||
)
|
)
|
||||||
|
if not scope_policy.allowed:
|
||||||
|
workspace.restore_checkpoint(None)
|
||||||
task = build_retry_task(
|
task = build_retry_task(
|
||||||
report,
|
report,
|
||||||
RuntimeError(
|
RuntimeError(
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ def test_aider_command_cannot_mutate_git_configuration_or_send_analytics() -> No
|
|||||||
assert command[-4:] == ["--edit-format", "udiff", "--timeout", "180"]
|
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("<?php\n")
|
||||||
|
|
||||||
|
command = AiderAgent(settings()).command(
|
||||||
|
git_dir / "task.txt",
|
||||||
|
("app/Http/Controllers/ChatController.php", "missing.php"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert command[-2:] == ["--file", "app/Http/Controllers/ChatController.php"]
|
||||||
|
assert "missing.php" not in command
|
||||||
|
|
||||||
|
|
||||||
def test_aider_environment_does_not_include_database_credentials(tmp_path, monkeypatch) -> None:
|
def test_aider_environment_does_not_include_database_credentials(tmp_path, monkeypatch) -> None:
|
||||||
config = settings(workspace_root=tmp_path)
|
config = settings(workspace_root=tmp_path)
|
||||||
workspace = tmp_path / "repo"
|
workspace = tmp_path / "repo"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from lai_automation.models import FailureReport
|
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:
|
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 "staged-query workflows" in task
|
||||||
assert "Do not put an impossible cross-connection join" in task
|
assert "Do not put an impossible cross-connection join" in task
|
||||||
assert "preserving tenant isolation" 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",
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user