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
@@ -0,0 +1,224 @@
1
+ """Automatic fix application based on agent findings."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Dict
6
+
7
+ from devloop.core.config import AutonomousFixesConfig, config
8
+ from devloop.core.context_store import Finding, context_store
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AutoFix:
14
+ """Automatically applies safe fixes based on agent findings."""
15
+
16
+ def __init__(self):
17
+ self._fix_history = set() # Track applied fixes to avoid duplicates
18
+
19
+ async def apply_safe_fixes(self) -> Dict[str, int]:
20
+ """Apply all safe fixes from agent findings.
21
+
22
+ Returns:
23
+ Dict mapping agent types to number of fixes applied
24
+ """
25
+ # Check if autonomous fixes are enabled
26
+ global_config = config.get_global_config()
27
+ if (
28
+ not global_config.autonomous_fixes
29
+ or not global_config.autonomous_fixes.enabled
30
+ ):
31
+ logger.info("Autonomous fixes are disabled in configuration")
32
+ return {}
33
+
34
+ findings = await context_store.get_findings()
35
+ actionable_findings = [f for f in findings if f.auto_fixable]
36
+ applied_fixes: Dict[str, int] = {}
37
+
38
+ for finding in actionable_findings:
39
+ agent_type = finding.agent
40
+ if await self._apply_single_fix(
41
+ agent_type, finding, global_config.autonomous_fixes
42
+ ):
43
+ applied_fixes[agent_type] = applied_fixes.get(agent_type, 0) + 1
44
+ await context_store.clear_findings(file_filter=finding.file)
45
+
46
+ return applied_fixes
47
+
48
+ async def _apply_single_fix(
49
+ self,
50
+ agent_type: str,
51
+ finding: Finding,
52
+ autonomous_fixes_config: AutonomousFixesConfig,
53
+ ) -> bool:
54
+ """Apply a single fix if it's safe to do so."""
55
+ if finding.id in self._fix_history:
56
+ return False # Already applied
57
+
58
+ # Check if fix is safe based on safety level
59
+ if not self._is_safe_for_config(
60
+ agent_type, finding, autonomous_fixes_config.safety_level
61
+ ):
62
+ logger.info(
63
+ f"Skipping {agent_type} fix (not safe for current safety level): {finding.message}"
64
+ )
65
+ return False
66
+
67
+ # Apply the fix
68
+ success = await self._execute_fix(agent_type, finding)
69
+ if success:
70
+ self._fix_history.add(finding.id)
71
+ logger.info(f"Applied {agent_type} fix: {finding.message}")
72
+
73
+ return success
74
+
75
+ def _is_safe_for_config(
76
+ self, agent_type: str, finding: Finding, safety_level: str
77
+ ) -> bool:
78
+ """Check if a finding is safe to apply based on the configured safety level."""
79
+ message = finding.message.lower()
80
+
81
+ # Always reject errors/failures
82
+ if any(
83
+ keyword in message for keyword in ["error", "failed", "timeout", "conflict"]
84
+ ):
85
+ return False
86
+
87
+ if safety_level == "safe_only":
88
+ return self._is_safe_only_fix(agent_type, finding)
89
+ elif safety_level == "medium_risk":
90
+ return self._is_medium_risk_fix(agent_type, finding)
91
+ elif safety_level == "all":
92
+ return True # Apply all fixes
93
+ else:
94
+ return False # Unknown safety level, be safe
95
+
96
+ def _is_safe_only_fix(self, agent_type: str, finding: Finding) -> bool:
97
+ """Check if a finding is safe-only level."""
98
+ message = finding.message.lower()
99
+
100
+ if agent_type == "formatter":
101
+ # Only basic formatting issues
102
+ return "would format" in message or "needs formatting" in message
103
+
104
+ elif agent_type == "linter":
105
+ # Only very safe linting fixes
106
+ safe_patterns = [
107
+ "whitespace",
108
+ "indentation",
109
+ "trailing whitespace",
110
+ "blank line",
111
+ ]
112
+ return any(pattern in message for pattern in safe_patterns)
113
+
114
+ return False
115
+
116
+ def _is_medium_risk_fix(self, agent_type: str, finding: Finding) -> bool:
117
+ """Check if a finding is medium-risk level."""
118
+ message = finding.message.lower()
119
+
120
+ if agent_type == "formatter":
121
+ # All formatting fixes
122
+ return "would format" in message or "needs formatting" in message
123
+
124
+ elif agent_type == "linter":
125
+ # More linting fixes including imports
126
+ safe_patterns = [
127
+ "unused import",
128
+ "import sort",
129
+ "whitespace",
130
+ "indentation",
131
+ "trailing whitespace",
132
+ "blank line",
133
+ ]
134
+ return any(pattern in message for pattern in safe_patterns)
135
+
136
+ return False
137
+
138
+ async def _execute_fix(self, agent_type: str, finding: Finding) -> bool:
139
+ """Execute the actual fix."""
140
+ try:
141
+ if agent_type == "formatter":
142
+ return await self._execute_formatter_fix(finding)
143
+ elif agent_type == "linter":
144
+ return await self._execute_linter_fix(finding)
145
+ else:
146
+ return False
147
+ except Exception as e:
148
+ logger.error(f"Error applying {agent_type} fix: {e}")
149
+ return False
150
+
151
+ async def _execute_formatter_fix(self, finding: Finding) -> bool:
152
+ """Execute a formatter fix."""
153
+ file_path = finding.file
154
+ if not file_path:
155
+ return False
156
+
157
+ path = Path(file_path)
158
+ formatter = finding.context.get("formatter", "black")
159
+
160
+ # Import the formatter agent logic
161
+ from devloop.agents.formatter import FormatterAgent
162
+
163
+ # Create a temporary formatter instance to run the fix
164
+ agent = FormatterAgent(
165
+ name="auto-formatter",
166
+ triggers=[],
167
+ event_bus=None,
168
+ config={
169
+ "formatOnSave": True,
170
+ "reportOnly": False,
171
+ "filePatterns": ["**/*"],
172
+ "formatters": {path.suffix.lstrip("."): formatter},
173
+ },
174
+ )
175
+
176
+ # Run the formatter directly
177
+ success, error = await agent._run_formatter(formatter, path)
178
+ return success
179
+
180
+ async def _execute_linter_fix(self, finding: Finding) -> bool:
181
+ """Execute a linter fix."""
182
+ # For now, we'll implement basic auto-fixes
183
+ # In a full implementation, this would use ruff --fix or similar
184
+ file_path = finding.file
185
+ if not file_path:
186
+ return False
187
+
188
+ message = finding.message.lower()
189
+
190
+ # Handle specific fix types
191
+ if "unused import" in message:
192
+ return await self._fix_unused_import(file_path, finding.context)
193
+ elif "import sort" in message:
194
+ return await self._fix_import_sort(file_path)
195
+ elif "whitespace" in message or "indentation" in message:
196
+ return await self._fix_whitespace(file_path, finding.context)
197
+
198
+ return False
199
+
200
+ async def _fix_unused_import(self, file_path: str, context: Dict) -> bool:
201
+ """Fix unused import."""
202
+ # This would need more sophisticated parsing
203
+ # For now, return False to be safe
204
+ return False
205
+
206
+ async def _fix_import_sort(self, file_path: str) -> bool:
207
+ """Fix import sorting."""
208
+ # Could use isort or similar
209
+ return False
210
+
211
+ async def _fix_whitespace(self, file_path: str, context: Dict) -> bool:
212
+ """Fix whitespace issues."""
213
+ # Could use autopep8 or similar
214
+ return False
215
+
216
+
217
+ # Global instance
218
+ auto_fix = AutoFix()
219
+
220
+
221
+ async def apply_safe_fixes():
222
+ """Convenience function to apply safe fixes."""
223
+ results = await auto_fix.apply_safe_fixes()
224
+ return results
devloop/core/config.py ADDED
@@ -0,0 +1,272 @@
1
+ """Configuration management for devloop."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class AutonomousFixesConfig:
12
+ """Configuration for autonomous fix application."""
13
+
14
+ enabled: bool = False
15
+ safety_level: str = "safe_only"
16
+
17
+ def __post_init__(self):
18
+ if self.safety_level not in ["safe_only", "medium_risk", "all"]:
19
+ raise ValueError(f"Invalid safety_level: {self.safety_level}")
20
+
21
+
22
+ @dataclass
23
+ class GlobalConfig:
24
+ """Global configuration."""
25
+
26
+ mode: str = "report-only"
27
+ max_concurrent_agents: int = 5
28
+ notification_level: str = "summary"
29
+ context_store_enabled: bool = True
30
+ context_store_path: str = ".devloop/context"
31
+ autonomous_fixes: Optional[AutonomousFixesConfig] = None
32
+
33
+ def __post_init__(self):
34
+ if self.mode not in ["report-only", "active"]:
35
+ raise ValueError(f"Invalid mode: {self.mode}")
36
+ if self.notification_level not in ["none", "summary", "detailed"]:
37
+ raise ValueError(f"Invalid notification_level: {self.notification_level}")
38
+ if self.autonomous_fixes is None:
39
+ self.autonomous_fixes = AutonomousFixesConfig()
40
+
41
+
42
+ class Config:
43
+ """Main configuration manager."""
44
+
45
+ def __init__(self, config_path: str = ".devloop/agents.json"):
46
+ self.config_path = Path(config_path)
47
+ self._config: Optional[Dict[str, Any]] = None
48
+
49
+ def load(self) -> Dict[str, Any]:
50
+ """Load configuration from file."""
51
+ # Always reload for development - remove caching for now
52
+ if not self.config_path.exists():
53
+ # Return default config
54
+ return self._get_default_config()
55
+ else:
56
+ try:
57
+ with open(self.config_path, "r") as f:
58
+ return json.load(f)
59
+ except (json.JSONDecodeError, IOError) as e:
60
+ print(f"Warning: Could not load config from {self.config_path}: {e}")
61
+ return self._get_default_config()
62
+
63
+ def get_global_config(self) -> GlobalConfig:
64
+ """Get global configuration."""
65
+ config = self.load()
66
+ global_config = config.get("global", {})
67
+
68
+ autonomous_fixes_config = global_config.get("autonomousFixes", {})
69
+ autonomous_fixes = AutonomousFixesConfig(
70
+ enabled=autonomous_fixes_config.get("enabled", False),
71
+ safety_level=autonomous_fixes_config.get("safetyLevel", "safe_only"),
72
+ )
73
+
74
+ return GlobalConfig(
75
+ mode=global_config.get("mode", "report-only"),
76
+ max_concurrent_agents=global_config.get("maxConcurrentAgents", 5),
77
+ notification_level=global_config.get("notificationLevel", "summary"),
78
+ context_store_enabled=global_config.get("contextStore", {}).get(
79
+ "enabled", True
80
+ ),
81
+ context_store_path=global_config.get("contextStore", {}).get(
82
+ "path", ".devloop/context"
83
+ ),
84
+ autonomous_fixes=autonomous_fixes,
85
+ )
86
+
87
+ @staticmethod
88
+ def default_config() -> "Config":
89
+ """Get default configuration instance."""
90
+ config = Config()
91
+ config._config = config._get_default_config()
92
+ return config
93
+
94
+ def save(self, path: Path) -> None:
95
+ """Save configuration to file."""
96
+ path.parent.mkdir(parents=True, exist_ok=True)
97
+ with open(path, "w") as f:
98
+ json.dump(self._config, f, indent=2)
99
+
100
+ def _get_default_config(self) -> Dict[str, Any]:
101
+ """Get default configuration."""
102
+ return {
103
+ "version": "1.0.0",
104
+ "enabled": True,
105
+ "agents": {
106
+ "linter": {
107
+ "enabled": True,
108
+ "triggers": ["file:modified", "file:created"],
109
+ "config": {
110
+ "autoFix": False,
111
+ "reportOnly": True,
112
+ "filePatterns": ["**/*.py", "**/*.js", "**/*.ts"],
113
+ "linters": {
114
+ "python": "ruff",
115
+ "javascript": "eslint",
116
+ "typescript": "eslint",
117
+ },
118
+ },
119
+ },
120
+ "formatter": {
121
+ "enabled": True,
122
+ "triggers": ["file:modified"],
123
+ "config": {
124
+ "formatOnSave": False,
125
+ "reportOnly": True,
126
+ "filePatterns": ["**/*.py", "**/*.js", "**/*.ts"],
127
+ "formatters": {
128
+ "python": "black",
129
+ "javascript": "prettier",
130
+ "typescript": "prettier",
131
+ },
132
+ },
133
+ },
134
+ "test-runner": {
135
+ "enabled": True,
136
+ "triggers": ["file:modified", "file:created"],
137
+ "config": {
138
+ "runOnSave": True,
139
+ "relatedTestsOnly": True,
140
+ "testFrameworks": {
141
+ "python": "pytest",
142
+ "javascript": "jest",
143
+ "typescript": "jest",
144
+ },
145
+ },
146
+ },
147
+ "agent-health-monitor": {
148
+ "enabled": True,
149
+ "triggers": ["agent:*:completed"],
150
+ "config": {"monitorAllAgents": True, "autoFixOnFailure": True},
151
+ },
152
+ "type-checker": {
153
+ "enabled": True,
154
+ "triggers": ["file:modified", "file:created"],
155
+ "config": {
156
+ "enabled_tools": ["mypy"],
157
+ "strict_mode": False,
158
+ "show_error_codes": True,
159
+ "exclude_patterns": ["test_*", "*_test.py", "*/tests/*"],
160
+ "max_issues": 50,
161
+ },
162
+ },
163
+ "security-scanner": {
164
+ "enabled": True,
165
+ "triggers": ["file:modified", "file:created"],
166
+ "config": {
167
+ "enabled_tools": ["bandit"],
168
+ "severity_threshold": "medium",
169
+ "confidence_threshold": "medium",
170
+ "exclude_patterns": ["test_*", "*_test.py", "*/tests/*"],
171
+ "max_issues": 50,
172
+ },
173
+ },
174
+ "git-commit-assistant": {
175
+ "enabled": True,
176
+ "triggers": ["git:pre-commit", "git:commit"],
177
+ "config": {
178
+ "conventional_commits": True,
179
+ "max_message_length": 72,
180
+ "include_breaking_changes": True,
181
+ "analyze_file_changes": True,
182
+ "auto_generate_scope": True,
183
+ "common_types": [
184
+ "feat",
185
+ "fix",
186
+ "docs",
187
+ "style",
188
+ "refactor",
189
+ "test",
190
+ "chore",
191
+ "perf",
192
+ "ci",
193
+ "build",
194
+ ],
195
+ },
196
+ },
197
+ "performance-profiler": {
198
+ "enabled": True,
199
+ "triggers": ["file:modified", "file:created"],
200
+ "config": {
201
+ "complexity_threshold": 10,
202
+ "min_lines_threshold": 50,
203
+ "enabled_tools": ["radon"],
204
+ "exclude_patterns": [
205
+ "test_*",
206
+ "*_test.py",
207
+ "*/tests/*",
208
+ "__init__.py",
209
+ ],
210
+ "max_issues": 50,
211
+ },
212
+ },
213
+ },
214
+ "global": {
215
+ "mode": "report-only",
216
+ "maxConcurrentAgents": 5,
217
+ "notificationLevel": "summary",
218
+ "resourceLimits": {},
219
+ "logging": {},
220
+ "contextStore": {"enabled": True, "path": ".devloop/context"},
221
+ "autonomousFixes": {"enabled": True, "safetyLevel": "safe_only"},
222
+ },
223
+ "eventSystem": {"collectors": {}, "dispatcher": {}, "store": {}},
224
+ }
225
+
226
+
227
+ class ConfigWrapper:
228
+ """Wrapper around config dictionary to provide object-like access."""
229
+
230
+ def __init__(self, config_dict: Dict[str, Any]):
231
+ self._config = config_dict
232
+
233
+ def is_agent_enabled(self, agent_name: str) -> bool:
234
+ """Check if an agent is enabled."""
235
+ agents = self._config.get("agents", {})
236
+ agent_config = agents.get(agent_name, {})
237
+ return agent_config.get("enabled", False)
238
+
239
+ def get_agent_config(self, agent_name: str) -> Optional[Dict[str, Any]]:
240
+ """Get configuration for a specific agent."""
241
+ agents = self._config.get("agents", {})
242
+ return agents.get(agent_name)
243
+
244
+ def get_global_config(self) -> GlobalConfig:
245
+ """Get global configuration."""
246
+ global_config = self._config.get("global", {})
247
+ autonomous_fixes_config = global_config.get("autonomousFixes", {})
248
+ autonomous_fixes = AutonomousFixesConfig(
249
+ enabled=autonomous_fixes_config.get("enabled", False),
250
+ safety_level=autonomous_fixes_config.get("safetyLevel", "safe_only"),
251
+ )
252
+
253
+ return GlobalConfig(
254
+ mode=global_config.get("mode", "report-only"),
255
+ max_concurrent_agents=global_config.get("maxConcurrentAgents", 5),
256
+ notification_level=global_config.get("notificationLevel", "summary"),
257
+ context_store_enabled=global_config.get("contextStore", {}).get(
258
+ "enabled", True
259
+ ),
260
+ context_store_path=global_config.get("contextStore", {}).get(
261
+ "path", ".devloop/context"
262
+ ),
263
+ autonomous_fixes=autonomous_fixes,
264
+ )
265
+
266
+ def agents(self):
267
+ """Get agents dictionary."""
268
+ return self._config.get("agents", {})
269
+
270
+
271
+ # Global config instance
272
+ config = Config()
File without changes