devloop 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devloop/__init__.py +3 -0
- devloop/agents/__init__.py +33 -0
- devloop/agents/agent_health_monitor.py +105 -0
- devloop/agents/ci_monitor.py +237 -0
- devloop/agents/code_rabbit.py +248 -0
- devloop/agents/doc_lifecycle.py +374 -0
- devloop/agents/echo.py +24 -0
- devloop/agents/file_logger.py +46 -0
- devloop/agents/formatter.py +511 -0
- devloop/agents/git_commit_assistant.py +421 -0
- devloop/agents/linter.py +399 -0
- devloop/agents/performance_profiler.py +284 -0
- devloop/agents/security_scanner.py +322 -0
- devloop/agents/snyk.py +292 -0
- devloop/agents/test_runner.py +484 -0
- devloop/agents/type_checker.py +242 -0
- devloop/cli/__init__.py +1 -0
- devloop/cli/commands/__init__.py +1 -0
- devloop/cli/commands/custom_agents.py +144 -0
- devloop/cli/commands/feedback.py +161 -0
- devloop/cli/commands/summary.py +50 -0
- devloop/cli/main.py +430 -0
- devloop/cli/main_v1.py +144 -0
- devloop/collectors/__init__.py +17 -0
- devloop/collectors/base.py +55 -0
- devloop/collectors/filesystem.py +126 -0
- devloop/collectors/git.py +171 -0
- devloop/collectors/manager.py +159 -0
- devloop/collectors/process.py +221 -0
- devloop/collectors/system.py +195 -0
- devloop/core/__init__.py +21 -0
- devloop/core/agent.py +206 -0
- devloop/core/agent_template.py +498 -0
- devloop/core/amp_integration.py +166 -0
- devloop/core/auto_fix.py +224 -0
- devloop/core/config.py +272 -0
- devloop/core/context.py +0 -0
- devloop/core/context_store.py +530 -0
- devloop/core/contextual_feedback.py +311 -0
- devloop/core/custom_agent.py +439 -0
- devloop/core/debug_trace.py +289 -0
- devloop/core/event.py +105 -0
- devloop/core/event_store.py +316 -0
- devloop/core/feedback.py +311 -0
- devloop/core/learning.py +351 -0
- devloop/core/manager.py +219 -0
- devloop/core/performance.py +433 -0
- devloop/core/proactive_feedback.py +302 -0
- devloop/core/summary_formatter.py +159 -0
- devloop/core/summary_generator.py +275 -0
- devloop-0.2.0.dist-info/METADATA +705 -0
- devloop-0.2.0.dist-info/RECORD +55 -0
- devloop-0.2.0.dist-info/WHEEL +4 -0
- devloop-0.2.0.dist-info/entry_points.txt +3 -0
- devloop-0.2.0.dist-info/licenses/LICENSE +21 -0
devloop/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Built-in agents."""
|
|
2
|
+
|
|
3
|
+
from .agent_health_monitor import AgentHealthMonitorAgent
|
|
4
|
+
from .ci_monitor import CIMonitorAgent
|
|
5
|
+
from .code_rabbit import CodeRabbitAgent
|
|
6
|
+
from .doc_lifecycle import DocLifecycleAgent
|
|
7
|
+
from .echo import EchoAgent
|
|
8
|
+
from .file_logger import FileLoggerAgent
|
|
9
|
+
from .formatter import FormatterAgent
|
|
10
|
+
from .git_commit_assistant import GitCommitAssistantAgent
|
|
11
|
+
from .linter import LinterAgent
|
|
12
|
+
from .performance_profiler import PerformanceProfilerAgent
|
|
13
|
+
from .security_scanner import SecurityScannerAgent
|
|
14
|
+
from .snyk import SnykAgent
|
|
15
|
+
from .test_runner import TestRunnerAgent
|
|
16
|
+
from .type_checker import TypeCheckerAgent
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AgentHealthMonitorAgent",
|
|
20
|
+
"CIMonitorAgent",
|
|
21
|
+
"CodeRabbitAgent",
|
|
22
|
+
"DocLifecycleAgent",
|
|
23
|
+
"EchoAgent",
|
|
24
|
+
"FileLoggerAgent",
|
|
25
|
+
"FormatterAgent",
|
|
26
|
+
"GitCommitAssistantAgent",
|
|
27
|
+
"LinterAgent",
|
|
28
|
+
"PerformanceProfilerAgent",
|
|
29
|
+
"SecurityScannerAgent",
|
|
30
|
+
"SnykAgent",
|
|
31
|
+
"TestRunnerAgent",
|
|
32
|
+
"TypeCheckerAgent",
|
|
33
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Agent Health Monitor Agent - monitors other agents and triggers fixes on failures."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
from devloop.core.agent import Agent, AgentResult
|
|
7
|
+
from devloop.core.auto_fix import auto_fix
|
|
8
|
+
from devloop.core.context_store import context_store, Finding
|
|
9
|
+
from devloop.core.event import Event
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentHealthMonitorAgent(Agent):
|
|
16
|
+
"""Monitors other agents for failures and triggers autonomous fixes."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, name: str, triggers: list[str], event_bus, config: Dict[str, Any]
|
|
20
|
+
):
|
|
21
|
+
super().__init__(name, triggers, event_bus)
|
|
22
|
+
self.config = config
|
|
23
|
+
|
|
24
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
25
|
+
"""Handle agent completion events and trigger fixes on failures."""
|
|
26
|
+
try:
|
|
27
|
+
payload = event.payload
|
|
28
|
+
|
|
29
|
+
# Extract result information
|
|
30
|
+
agent_name = payload.get("agent_name", "")
|
|
31
|
+
success = payload.get("success", True)
|
|
32
|
+
error = payload.get("error", "")
|
|
33
|
+
message = payload.get("message", "")
|
|
34
|
+
|
|
35
|
+
# Skip if this is our own completion event to avoid loops
|
|
36
|
+
if agent_name == self.name:
|
|
37
|
+
return AgentResult(
|
|
38
|
+
agent_name=self.name,
|
|
39
|
+
success=True,
|
|
40
|
+
duration=0.0,
|
|
41
|
+
message="Skipped monitoring own completion",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Check if the agent failed
|
|
45
|
+
if not success or error:
|
|
46
|
+
logger.info(f"Detected failure in {agent_name}: {error or message}")
|
|
47
|
+
|
|
48
|
+
# Apply autonomous fixes
|
|
49
|
+
applied_fixes = await auto_fix.apply_safe_fixes()
|
|
50
|
+
|
|
51
|
+
if applied_fixes:
|
|
52
|
+
fix_summary = ", ".join(
|
|
53
|
+
f"{k}: {v}" for k, v in applied_fixes.items()
|
|
54
|
+
)
|
|
55
|
+
agent_result = AgentResult(
|
|
56
|
+
agent_name=self.name,
|
|
57
|
+
success=True,
|
|
58
|
+
duration=0.0,
|
|
59
|
+
message=f"Applied fixes for {agent_name} failure: {fix_summary}",
|
|
60
|
+
data={"applied_fixes": applied_fixes},
|
|
61
|
+
)
|
|
62
|
+
await context_store.add_finding(
|
|
63
|
+
Finding(
|
|
64
|
+
id=f"{self.name}-{agent_name}-failure-fix",
|
|
65
|
+
agent=self.name,
|
|
66
|
+
timestamp=str(event.timestamp),
|
|
67
|
+
file="",
|
|
68
|
+
message=agent_result.message,
|
|
69
|
+
context=agent_result.data or {},
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return agent_result
|
|
73
|
+
else:
|
|
74
|
+
agent_result = AgentResult(
|
|
75
|
+
agent_name=self.name,
|
|
76
|
+
success=True,
|
|
77
|
+
duration=0.0,
|
|
78
|
+
message=f"No safe fixes available for {agent_name} failure",
|
|
79
|
+
data={"applied_fixes": {}},
|
|
80
|
+
)
|
|
81
|
+
await context_store.add_finding(
|
|
82
|
+
Finding(
|
|
83
|
+
id=f"{self.name}-{agent_name}-no-fix",
|
|
84
|
+
agent=self.name,
|
|
85
|
+
timestamp=str(event.timestamp),
|
|
86
|
+
file="",
|
|
87
|
+
message=agent_result.message,
|
|
88
|
+
context=agent_result.data or {},
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
return agent_result
|
|
92
|
+
|
|
93
|
+
# Agent succeeded, nothing to do
|
|
94
|
+
return AgentResult(
|
|
95
|
+
agent_name=self.name,
|
|
96
|
+
success=True,
|
|
97
|
+
duration=0.0,
|
|
98
|
+
message=f"{agent_name} completed successfully",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Error in health monitor: {e}")
|
|
103
|
+
return AgentResult(
|
|
104
|
+
agent_name=self.name, success=False, duration=0.0, error=str(e)
|
|
105
|
+
)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""CI Monitor Agent - Monitors GitHub Actions CI status."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from devloop.core.agent import Agent, AgentResult
|
|
9
|
+
from devloop.core.context_store import Finding, Severity
|
|
10
|
+
from devloop.core.event import Event
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CIMonitorAgent(Agent):
|
|
14
|
+
"""Monitors GitHub Actions CI status and reports failures."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
name: str,
|
|
19
|
+
triggers: List[str],
|
|
20
|
+
event_bus: Any,
|
|
21
|
+
check_interval: int = 300,
|
|
22
|
+
): # 5 minutes default
|
|
23
|
+
"""
|
|
24
|
+
Initialize CI monitor agent.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Agent name
|
|
28
|
+
triggers: Event triggers for this agent
|
|
29
|
+
event_bus: Event bus for publishing events
|
|
30
|
+
check_interval: How often to check CI status (in seconds)
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(name, triggers, event_bus)
|
|
33
|
+
self.check_interval = check_interval
|
|
34
|
+
self.last_check: Optional[datetime] = None
|
|
35
|
+
self.last_status: Optional[dict] = None
|
|
36
|
+
|
|
37
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
38
|
+
"""Handle events and check CI status."""
|
|
39
|
+
# Check if it's time for periodic check
|
|
40
|
+
should_check = False
|
|
41
|
+
|
|
42
|
+
if event.type == "time:tick":
|
|
43
|
+
should_check = self._should_check_now()
|
|
44
|
+
elif event.type == "git:post-push":
|
|
45
|
+
# Always check after push
|
|
46
|
+
should_check = True
|
|
47
|
+
|
|
48
|
+
if not should_check:
|
|
49
|
+
return AgentResult(
|
|
50
|
+
agent_name=self.name,
|
|
51
|
+
success=True,
|
|
52
|
+
duration=0,
|
|
53
|
+
message="Not time to check CI yet",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Check CI status
|
|
57
|
+
try:
|
|
58
|
+
status = await self._check_ci_status()
|
|
59
|
+
self.last_check = datetime.now()
|
|
60
|
+
self.last_status = status
|
|
61
|
+
|
|
62
|
+
if not status:
|
|
63
|
+
return AgentResult(
|
|
64
|
+
agent_name=self.name,
|
|
65
|
+
success=True,
|
|
66
|
+
duration=0,
|
|
67
|
+
message="No CI runs found",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Analyze status and create findings
|
|
71
|
+
findings = self._analyze_ci_status(status)
|
|
72
|
+
|
|
73
|
+
if findings:
|
|
74
|
+
return AgentResult(
|
|
75
|
+
agent_name=self.name,
|
|
76
|
+
success=False,
|
|
77
|
+
duration=0,
|
|
78
|
+
message=f"CI issues detected: {len(findings)} workflows need attention",
|
|
79
|
+
data={"findings": findings},
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
return AgentResult(
|
|
83
|
+
agent_name=self.name,
|
|
84
|
+
success=True,
|
|
85
|
+
duration=0,
|
|
86
|
+
message="CI status: All checks passing",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return AgentResult(
|
|
91
|
+
agent_name=self.name,
|
|
92
|
+
success=False,
|
|
93
|
+
duration=0,
|
|
94
|
+
error=f"Failed to check CI status: {e}",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _should_check_now(self) -> bool:
|
|
98
|
+
"""Determine if we should check CI now based on interval."""
|
|
99
|
+
if self.last_check is None:
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
time_since_check = datetime.now() - self.last_check
|
|
103
|
+
return time_since_check > timedelta(seconds=self.check_interval)
|
|
104
|
+
|
|
105
|
+
async def _check_ci_status(self) -> Optional[dict]:
|
|
106
|
+
"""
|
|
107
|
+
Check CI status using gh CLI.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
CI status dict or None if unavailable
|
|
111
|
+
"""
|
|
112
|
+
# Check if gh CLI is available
|
|
113
|
+
try:
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
["gh", "--version"],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
timeout=5,
|
|
119
|
+
)
|
|
120
|
+
if result.returncode != 0:
|
|
121
|
+
return None
|
|
122
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Check if authenticated
|
|
126
|
+
try:
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
["gh", "auth", "status"],
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
timeout=5,
|
|
132
|
+
)
|
|
133
|
+
if result.returncode != 0:
|
|
134
|
+
return None
|
|
135
|
+
except subprocess.TimeoutExpired:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
# Get current branch
|
|
139
|
+
try:
|
|
140
|
+
result = subprocess.run(
|
|
141
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
check=True,
|
|
145
|
+
timeout=5,
|
|
146
|
+
)
|
|
147
|
+
branch = result.stdout.strip()
|
|
148
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# Get latest workflow runs
|
|
152
|
+
try:
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
[
|
|
155
|
+
"gh",
|
|
156
|
+
"run",
|
|
157
|
+
"list",
|
|
158
|
+
"--branch",
|
|
159
|
+
branch,
|
|
160
|
+
"--limit",
|
|
161
|
+
"5",
|
|
162
|
+
"--json",
|
|
163
|
+
"status,conclusion,name,databaseId,createdAt,workflowName",
|
|
164
|
+
],
|
|
165
|
+
capture_output=True,
|
|
166
|
+
text=True,
|
|
167
|
+
check=True,
|
|
168
|
+
timeout=10,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if result.stdout.strip():
|
|
172
|
+
runs = json.loads(result.stdout)
|
|
173
|
+
return {
|
|
174
|
+
"branch": branch,
|
|
175
|
+
"runs": runs,
|
|
176
|
+
"checked_at": datetime.now().isoformat(),
|
|
177
|
+
}
|
|
178
|
+
except (
|
|
179
|
+
subprocess.CalledProcessError,
|
|
180
|
+
subprocess.TimeoutExpired,
|
|
181
|
+
json.JSONDecodeError,
|
|
182
|
+
):
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def _analyze_ci_status(self, status: dict) -> list[Finding]:
|
|
188
|
+
"""Analyze CI status and create findings for issues."""
|
|
189
|
+
findings = []
|
|
190
|
+
runs = status.get("runs", [])
|
|
191
|
+
branch = status.get("branch", "unknown")
|
|
192
|
+
|
|
193
|
+
# Group runs by workflow
|
|
194
|
+
workflows: Dict[str, list] = {}
|
|
195
|
+
for run in runs:
|
|
196
|
+
workflow_name = run.get("workflowName", run.get("name", "Unknown"))
|
|
197
|
+
if workflow_name not in workflows:
|
|
198
|
+
workflows[workflow_name] = []
|
|
199
|
+
workflows[workflow_name].append(run)
|
|
200
|
+
|
|
201
|
+
# Check each workflow's latest run
|
|
202
|
+
for workflow_name, workflow_runs in workflows.items():
|
|
203
|
+
latest_run = workflow_runs[0] # Already sorted by createdAt descending
|
|
204
|
+
conclusion = latest_run.get("conclusion")
|
|
205
|
+
status_val = latest_run.get("status")
|
|
206
|
+
run_id = latest_run.get("databaseId", "unknown")
|
|
207
|
+
|
|
208
|
+
if conclusion == "failure":
|
|
209
|
+
findings.append(
|
|
210
|
+
Finding(
|
|
211
|
+
id=f"ci-{run_id}",
|
|
212
|
+
agent="ci-monitor",
|
|
213
|
+
timestamp=datetime.now().isoformat(),
|
|
214
|
+
file=".github/workflows/ci.yml",
|
|
215
|
+
category="ci",
|
|
216
|
+
severity=Severity.ERROR,
|
|
217
|
+
message=f"CI workflow '{workflow_name}' failed on branch '{branch}' (Run #{run_id})",
|
|
218
|
+
suggestion=f"View details: gh run view {run_id}\nRerun: gh run rerun {run_id}",
|
|
219
|
+
auto_fixable=False,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
elif status_val == "in_progress":
|
|
223
|
+
findings.append(
|
|
224
|
+
Finding(
|
|
225
|
+
id=f"ci-{run_id}",
|
|
226
|
+
agent="ci-monitor",
|
|
227
|
+
timestamp=datetime.now().isoformat(),
|
|
228
|
+
file=".github/workflows/ci.yml",
|
|
229
|
+
category="ci",
|
|
230
|
+
severity=Severity.INFO,
|
|
231
|
+
message=f"CI workflow '{workflow_name}' is running on branch '{branch}' (Run #{run_id})",
|
|
232
|
+
suggestion=f"Watch progress: gh run watch {run_id}",
|
|
233
|
+
auto_fixable=False,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return findings
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Code Rabbit agent - integrates Code Rabbit CLI for code analysis."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
from devloop.core.agent import Agent, AgentResult
|
|
10
|
+
from devloop.core.context_store import Finding, Severity
|
|
11
|
+
from devloop.core.event import Event
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CodeRabbitConfig:
|
|
15
|
+
"""Configuration for CodeRabbitAgent."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: Dict[str, Any]):
|
|
18
|
+
self.enabled = config.get("enabled", True)
|
|
19
|
+
self.api_key = config.get("apiKey")
|
|
20
|
+
self.min_severity = config.get("minSeverity", "warning")
|
|
21
|
+
self.file_patterns = config.get(
|
|
22
|
+
"filePatterns", ["**/*.py", "**/*.js", "**/*.ts"]
|
|
23
|
+
)
|
|
24
|
+
self.debounce = config.get("debounce", 500) # ms
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CodeRabbitResult:
|
|
28
|
+
"""Result from running Code Rabbit analysis."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
success: bool,
|
|
33
|
+
issues: List[Dict[str, Any]] | None = None,
|
|
34
|
+
error: str | None = None,
|
|
35
|
+
):
|
|
36
|
+
self.success = success
|
|
37
|
+
self.issues = issues or []
|
|
38
|
+
self.error = error
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def has_issues(self) -> bool:
|
|
42
|
+
"""Check if there are any issues."""
|
|
43
|
+
return len(self.issues) > 0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def issue_count(self) -> int:
|
|
47
|
+
"""Get number of issues."""
|
|
48
|
+
return len(self.issues)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CodeRabbitAgent(Agent):
|
|
52
|
+
"""Agent that runs Code Rabbit analysis on file changes."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
name: str,
|
|
57
|
+
triggers: List[str],
|
|
58
|
+
event_bus,
|
|
59
|
+
config: Dict[str, Any] | None = None,
|
|
60
|
+
feedback_api=None,
|
|
61
|
+
performance_monitor=None,
|
|
62
|
+
):
|
|
63
|
+
super().__init__(
|
|
64
|
+
name,
|
|
65
|
+
triggers,
|
|
66
|
+
event_bus,
|
|
67
|
+
feedback_api=feedback_api,
|
|
68
|
+
performance_monitor=performance_monitor,
|
|
69
|
+
)
|
|
70
|
+
self.config = CodeRabbitConfig(config or {})
|
|
71
|
+
self._last_run: Dict[str, float] = {} # path -> timestamp for debouncing
|
|
72
|
+
|
|
73
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
74
|
+
"""Handle file change event by running Code Rabbit analysis."""
|
|
75
|
+
# Extract file path
|
|
76
|
+
file_path = event.payload.get("path")
|
|
77
|
+
if not file_path:
|
|
78
|
+
return AgentResult(
|
|
79
|
+
agent_name=self.name,
|
|
80
|
+
success=True,
|
|
81
|
+
duration=0,
|
|
82
|
+
message="No file path in event",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
path = Path(file_path)
|
|
86
|
+
|
|
87
|
+
# Check if file should be analyzed
|
|
88
|
+
if not self._should_analyze(path):
|
|
89
|
+
return AgentResult(
|
|
90
|
+
agent_name=self.name,
|
|
91
|
+
success=True,
|
|
92
|
+
duration=0,
|
|
93
|
+
message=f"Skipped {path.name} (not in patterns)",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Run Code Rabbit analysis
|
|
97
|
+
result = await self._run_code_rabbit(path)
|
|
98
|
+
|
|
99
|
+
# Build result message
|
|
100
|
+
if result.error:
|
|
101
|
+
message = f"Code Rabbit error on {path.name}: {result.error}"
|
|
102
|
+
success = False
|
|
103
|
+
elif result.has_issues:
|
|
104
|
+
message = f"Found {result.issue_count} issue(s) in {path.name}"
|
|
105
|
+
success = True
|
|
106
|
+
else:
|
|
107
|
+
message = f"No issues in {path.name}"
|
|
108
|
+
success = True
|
|
109
|
+
|
|
110
|
+
agent_result = AgentResult(
|
|
111
|
+
agent_name=self.name,
|
|
112
|
+
success=success,
|
|
113
|
+
duration=0,
|
|
114
|
+
message=message,
|
|
115
|
+
data={
|
|
116
|
+
"file": str(path),
|
|
117
|
+
"tool": "code-rabbit",
|
|
118
|
+
"issues": result.issues,
|
|
119
|
+
"issue_count": result.issue_count,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Write findings to context store for Claude Code integration
|
|
124
|
+
await self._write_findings_to_context(path, result)
|
|
125
|
+
|
|
126
|
+
return agent_result
|
|
127
|
+
|
|
128
|
+
def _should_analyze(self, path: Path) -> bool:
|
|
129
|
+
"""Check if file should be analyzed based on patterns."""
|
|
130
|
+
# Skip if file doesn't exist
|
|
131
|
+
if not path.exists():
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# Simple pattern matching
|
|
135
|
+
suffix = path.suffix
|
|
136
|
+
for pattern in self.config.file_patterns:
|
|
137
|
+
if pattern.endswith(suffix):
|
|
138
|
+
return True
|
|
139
|
+
if "*" in pattern and suffix in pattern:
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
async def _run_code_rabbit(self, path: Path) -> CodeRabbitResult:
|
|
145
|
+
"""Run Code Rabbit analysis on a file."""
|
|
146
|
+
try:
|
|
147
|
+
# Check if code-rabbit CLI is installed
|
|
148
|
+
check = await asyncio.create_subprocess_exec(
|
|
149
|
+
"code-rabbit",
|
|
150
|
+
"--version",
|
|
151
|
+
stdout=asyncio.subprocess.PIPE,
|
|
152
|
+
stderr=asyncio.subprocess.PIPE,
|
|
153
|
+
)
|
|
154
|
+
await check.communicate()
|
|
155
|
+
|
|
156
|
+
if check.returncode != 0:
|
|
157
|
+
return CodeRabbitResult(
|
|
158
|
+
success=False, error="code-rabbit CLI not installed"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Run Code Rabbit with JSON output
|
|
162
|
+
proc = await asyncio.create_subprocess_exec(
|
|
163
|
+
"code-rabbit",
|
|
164
|
+
"analyze",
|
|
165
|
+
"--format",
|
|
166
|
+
"json",
|
|
167
|
+
str(path),
|
|
168
|
+
stdout=asyncio.subprocess.PIPE,
|
|
169
|
+
stderr=asyncio.subprocess.PIPE,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
stdout, stderr = await proc.communicate()
|
|
173
|
+
|
|
174
|
+
if proc.returncode != 0 and not stdout:
|
|
175
|
+
return CodeRabbitResult(
|
|
176
|
+
success=False,
|
|
177
|
+
error=f"code-rabbit failed: {stderr.decode() if stderr else 'unknown error'}",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if stdout:
|
|
181
|
+
try:
|
|
182
|
+
data = json.loads(stdout.decode())
|
|
183
|
+
issues = data.get("issues", []) if isinstance(data, dict) else data
|
|
184
|
+
return CodeRabbitResult(success=True, issues=issues)
|
|
185
|
+
except json.JSONDecodeError as e:
|
|
186
|
+
return CodeRabbitResult(
|
|
187
|
+
success=False, error=f"Failed to parse Code Rabbit output: {e}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return CodeRabbitResult(success=True, issues=[])
|
|
191
|
+
|
|
192
|
+
except FileNotFoundError:
|
|
193
|
+
return CodeRabbitResult(
|
|
194
|
+
success=False, error="code-rabbit command not found"
|
|
195
|
+
)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self.logger.error(f"Error running Code Rabbit: {e}")
|
|
198
|
+
return CodeRabbitResult(success=False, error=str(e))
|
|
199
|
+
|
|
200
|
+
async def _write_findings_to_context(
|
|
201
|
+
self, path: Path, result: CodeRabbitResult
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Write Code Rabbit findings to context store."""
|
|
204
|
+
if not result.success or not result.has_issues:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
from devloop.core.context_store import context_store
|
|
208
|
+
|
|
209
|
+
# Convert each issue to a Finding
|
|
210
|
+
for idx, issue in enumerate(result.issues):
|
|
211
|
+
# Extract issue details
|
|
212
|
+
line = issue.get("line")
|
|
213
|
+
column = issue.get("column")
|
|
214
|
+
code = issue.get("code", "unknown")
|
|
215
|
+
message_text = issue.get("message", "")
|
|
216
|
+
severity_str = issue.get("severity", "warning").lower()
|
|
217
|
+
|
|
218
|
+
# Map severity values
|
|
219
|
+
severity_map = {
|
|
220
|
+
"error": Severity.ERROR,
|
|
221
|
+
"warning": Severity.WARNING,
|
|
222
|
+
"info": Severity.INFO,
|
|
223
|
+
"note": Severity.INFO,
|
|
224
|
+
}
|
|
225
|
+
severity = severity_map.get(severity_str, Severity.WARNING)
|
|
226
|
+
|
|
227
|
+
# Create Finding
|
|
228
|
+
finding = Finding(
|
|
229
|
+
id=f"{self.name}-{path}-{line}-{code}",
|
|
230
|
+
agent=self.name,
|
|
231
|
+
timestamp=str(datetime.now()),
|
|
232
|
+
file=str(path),
|
|
233
|
+
line=line,
|
|
234
|
+
column=column,
|
|
235
|
+
severity=severity,
|
|
236
|
+
message=message_text,
|
|
237
|
+
category=code,
|
|
238
|
+
suggestion=issue.get("suggestion", ""),
|
|
239
|
+
context={
|
|
240
|
+
"tool": "code-rabbit",
|
|
241
|
+
"issue_type": issue.get("type", "unknown"),
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
await context_store.add_finding(finding)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
self.logger.error(f"Failed to write finding to context: {e}")
|