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.
Files changed (55) hide show
  1. devloop/__init__.py +3 -0
  2. devloop/agents/__init__.py +33 -0
  3. devloop/agents/agent_health_monitor.py +105 -0
  4. devloop/agents/ci_monitor.py +237 -0
  5. devloop/agents/code_rabbit.py +248 -0
  6. devloop/agents/doc_lifecycle.py +374 -0
  7. devloop/agents/echo.py +24 -0
  8. devloop/agents/file_logger.py +46 -0
  9. devloop/agents/formatter.py +511 -0
  10. devloop/agents/git_commit_assistant.py +421 -0
  11. devloop/agents/linter.py +399 -0
  12. devloop/agents/performance_profiler.py +284 -0
  13. devloop/agents/security_scanner.py +322 -0
  14. devloop/agents/snyk.py +292 -0
  15. devloop/agents/test_runner.py +484 -0
  16. devloop/agents/type_checker.py +242 -0
  17. devloop/cli/__init__.py +1 -0
  18. devloop/cli/commands/__init__.py +1 -0
  19. devloop/cli/commands/custom_agents.py +144 -0
  20. devloop/cli/commands/feedback.py +161 -0
  21. devloop/cli/commands/summary.py +50 -0
  22. devloop/cli/main.py +430 -0
  23. devloop/cli/main_v1.py +144 -0
  24. devloop/collectors/__init__.py +17 -0
  25. devloop/collectors/base.py +55 -0
  26. devloop/collectors/filesystem.py +126 -0
  27. devloop/collectors/git.py +171 -0
  28. devloop/collectors/manager.py +159 -0
  29. devloop/collectors/process.py +221 -0
  30. devloop/collectors/system.py +195 -0
  31. devloop/core/__init__.py +21 -0
  32. devloop/core/agent.py +206 -0
  33. devloop/core/agent_template.py +498 -0
  34. devloop/core/amp_integration.py +166 -0
  35. devloop/core/auto_fix.py +224 -0
  36. devloop/core/config.py +272 -0
  37. devloop/core/context.py +0 -0
  38. devloop/core/context_store.py +530 -0
  39. devloop/core/contextual_feedback.py +311 -0
  40. devloop/core/custom_agent.py +439 -0
  41. devloop/core/debug_trace.py +289 -0
  42. devloop/core/event.py +105 -0
  43. devloop/core/event_store.py +316 -0
  44. devloop/core/feedback.py +311 -0
  45. devloop/core/learning.py +351 -0
  46. devloop/core/manager.py +219 -0
  47. devloop/core/performance.py +433 -0
  48. devloop/core/proactive_feedback.py +302 -0
  49. devloop/core/summary_formatter.py +159 -0
  50. devloop/core/summary_generator.py +275 -0
  51. devloop-0.2.0.dist-info/METADATA +705 -0
  52. devloop-0.2.0.dist-info/RECORD +55 -0
  53. devloop-0.2.0.dist-info/WHEEL +4 -0
  54. devloop-0.2.0.dist-info/entry_points.txt +3 -0
  55. devloop-0.2.0.dist-info/licenses/LICENSE +21 -0
devloop/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """DevLoop - Background agents for development workflow automation."""
2
+
3
+ __version__ = "0.2.0"
@@ -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}")