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,242 @@
1
+ #!/usr/bin/env python3
2
+ """Type Checker Agent - Runs static type checking on code."""
3
+
4
+ import logging
5
+ import subprocess # nosec B404 - Required for running type checking tools
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Dict, Any, List, Optional
9
+ from dataclasses import dataclass, field
10
+
11
+ from ..core.agent import Agent, AgentResult
12
+ from ..core.event import Event
13
+
14
+
15
+ @dataclass
16
+ class TypeCheckerConfig:
17
+ """Configuration for type checking."""
18
+
19
+ enabled_tools: List[str] = field(default_factory=lambda: ["mypy"])
20
+ strict_mode: bool = False
21
+ show_error_codes: bool = True
22
+ exclude_patterns: List[str] = field(
23
+ default_factory=lambda: ["test*", "*_test.py", "*/tests/*"]
24
+ )
25
+ max_issues: int = 50
26
+
27
+
28
+ class TypeCheckResult:
29
+ """Type check result."""
30
+
31
+ def __init__(
32
+ self, tool: str, issues: List[Dict[str, Any]], errors: List[str] = None
33
+ ):
34
+ self.tool = tool
35
+ self.issues = issues
36
+ self.errors = errors or []
37
+ self.timestamp = None
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ return {
41
+ "tool": self.tool,
42
+ "issues_found": len(self.issues),
43
+ "issues": self.issues,
44
+ "errors": self.errors,
45
+ "severity_breakdown": self._get_severity_breakdown(),
46
+ "summary": f"Found {len(self.issues)} type issues",
47
+ }
48
+
49
+ def _get_severity_breakdown(self) -> Dict[str, int]:
50
+ breakdown = {"error": 0, "warning": 0, "note": 0, "unknown": 0}
51
+ for issue in self.issues:
52
+ severity = issue.get("severity", "unknown").lower()
53
+ breakdown[severity] = breakdown.get(severity, 0) + 1
54
+ return breakdown
55
+
56
+
57
+ class TypeCheckerAgent(Agent):
58
+ """Agent for running type checkers on code."""
59
+
60
+ def __init__(self, config: Dict[str, Any], event_bus):
61
+ super().__init__("type-checker", ["file:modified", "file:created"], event_bus)
62
+ self.config = TypeCheckerConfig(**config)
63
+ self.logger = logging.getLogger(f"agent.{self.name}")
64
+
65
+ async def handle(self, event: Event) -> AgentResult:
66
+ """Handle file change events by running type checks."""
67
+ try:
68
+ file_path = event.payload.get("path")
69
+ if not file_path:
70
+ return AgentResult(
71
+ agent_name=self.name,
72
+ success=False,
73
+ duration=0.0,
74
+ message="No file path in event",
75
+ )
76
+
77
+ path = Path(file_path)
78
+ if not path.exists():
79
+ return AgentResult(
80
+ agent_name=self.name,
81
+ success=False,
82
+ duration=0.0,
83
+ message=f"File does not exist: {file_path}",
84
+ )
85
+
86
+ # Only check Python files for now
87
+ if path.suffix != ".py":
88
+ return AgentResult(
89
+ agent_name=self.name,
90
+ success=True,
91
+ duration=0.0,
92
+ message=f"Skipped non-Python file: {file_path}",
93
+ )
94
+
95
+ # Check if file matches exclude patterns
96
+ if self._should_exclude_file(str(path)):
97
+ return AgentResult(
98
+ agent_name=self.name,
99
+ success=True,
100
+ duration=0.0,
101
+ message=f"Excluded file: {file_path}",
102
+ )
103
+
104
+ # Run type check
105
+ results = await self._run_type_check(path)
106
+
107
+ agent_result = AgentResult(
108
+ agent_name=self.name,
109
+ success=True,
110
+ duration=0.0, # Would be calculated in real implementation
111
+ message=f"Type checked {file_path} with {results.tool}",
112
+ data={
113
+ "file": str(path),
114
+ "tool": results.tool,
115
+ "issues_found": len(results.issues),
116
+ "issues": results.issues,
117
+ "severity_breakdown": results._get_severity_breakdown(),
118
+ "errors": results.errors,
119
+ },
120
+ )
121
+
122
+ return agent_result
123
+ except Exception as e:
124
+ self.logger.error(
125
+ f"Error handling type check for {event.payload.get('path', 'unknown')}: {e}",
126
+ exc_info=True,
127
+ )
128
+ return AgentResult(
129
+ agent_name=self.name,
130
+ success=False,
131
+ duration=0.0,
132
+ message=f"Type check failed: {str(e)}",
133
+ error=str(e),
134
+ )
135
+
136
+ def _should_exclude_file(self, file_path: str) -> bool:
137
+ """Check if file should be excluded from type checking."""
138
+ if not self.config.exclude_patterns:
139
+ return False
140
+ for pattern in self.config.exclude_patterns:
141
+ if pattern.startswith("*") and pattern.endswith("*"):
142
+ if pattern[1:-1] in file_path:
143
+ return True
144
+ elif pattern.startswith("*"):
145
+ if file_path.endswith(pattern[1:]):
146
+ return True
147
+ elif pattern.endswith("*"):
148
+ if file_path.startswith(pattern[:-1]):
149
+ return True
150
+ elif pattern == file_path:
151
+ return True
152
+ return False
153
+
154
+ async def _run_type_check(self, file_path: Path) -> TypeCheckResult:
155
+ """Run type checking tools."""
156
+ results = []
157
+
158
+ # Try mypy first (most common Python type checker)
159
+ if "mypy" in self.config.enabled_tools:
160
+ mypy_result = self._run_mypy(file_path)
161
+ if mypy_result:
162
+ results.append(mypy_result)
163
+
164
+ # If no results from primary tools, return empty
165
+ if results:
166
+ return results[0] # Return first successful result
167
+
168
+ return TypeCheckResult("none", [], ["No type checking tools available"])
169
+
170
+ def _run_mypy(self, file_path: Path) -> Optional[TypeCheckResult]:
171
+ """Run MyPy type checker."""
172
+ try:
173
+ # Check if mypy is available
174
+ result = subprocess.run(
175
+ [sys.executable, "-c", "import mypy"], capture_output=True, text=True
176
+ ) # nosec B603 - Running trusted system Python with safe arguments
177
+ if result.returncode != 0:
178
+ return TypeCheckResult(
179
+ "mypy", [], ["MyPy not installed - run: pip install mypy"]
180
+ )
181
+
182
+ cmd = [
183
+ sys.executable,
184
+ "-m",
185
+ "mypy",
186
+ str(file_path),
187
+ "--show-error-codes",
188
+ "--no-error-summary",
189
+ ]
190
+
191
+ if self.config.strict_mode:
192
+ cmd.append("--strict")
193
+
194
+ # Run mypy in subprocess
195
+ result = subprocess.run(
196
+ cmd,
197
+ capture_output=True,
198
+ text=True,
199
+ cwd=file_path.parent,
200
+ ) # nosec B603 - Running mypy with controlled command arguments
201
+ issues = []
202
+
203
+ # Parse mypy output (line by line)
204
+ output_lines = result.stdout.strip().split("\n")
205
+ for line in output_lines:
206
+ if line.strip() and not line.startswith("Success:"):
207
+ # Parse mypy error format: file:line: error: message [error-code]
208
+ parts = line.split(":", 3)
209
+ if len(parts) >= 4:
210
+ filename = parts[0].strip()
211
+ try:
212
+ line_number = int(parts[1].strip())
213
+ except ValueError:
214
+ line_number = 0
215
+
216
+ error_type = parts[2].strip()
217
+ message_and_code = parts[3].strip()
218
+
219
+ # Extract error code if present
220
+ error_code = ""
221
+ if "[" in message_and_code and "]" in message_and_code:
222
+ message, code_part = message_and_code.rsplit("[", 1)
223
+ error_code = code_part.rstrip("]")
224
+ message = message.strip()
225
+ else:
226
+ message = message_and_code
227
+
228
+ issues.append(
229
+ {
230
+ "filename": filename,
231
+ "line_number": line_number,
232
+ "severity": error_type,
233
+ "message": message,
234
+ "error_code": error_code,
235
+ "tool": "mypy",
236
+ }
237
+ )
238
+
239
+ return TypeCheckResult("mypy", issues[: self.config.max_issues])
240
+
241
+ except Exception as e:
242
+ return TypeCheckResult("mypy", [], [f"MyPy execution error: {str(e)}"])
@@ -0,0 +1 @@
1
+ """CLI interface."""
@@ -0,0 +1 @@
1
+ """CLI commands package."""
@@ -0,0 +1,144 @@
1
+ """CLI commands for custom agent management."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from devloop.core.custom_agent import (
12
+ AgentBuilder,
13
+ CustomAgentStore,
14
+ get_agent_template,
15
+ )
16
+
17
+ app = typer.Typer(help="Manage custom agents")
18
+ console = Console()
19
+
20
+
21
+ @app.command()
22
+ def list_agents(
23
+ project_dir: Optional[Path] = typer.Option(None, help="Project directory"),
24
+ ):
25
+ """List all custom agents."""
26
+
27
+ async def _run():
28
+ nonlocal project_dir
29
+ if project_dir is None:
30
+ project_dir = Path.cwd()
31
+
32
+ storage_path = project_dir / ".devloop" / "custom_agents"
33
+ store = CustomAgentStore(storage_path)
34
+
35
+ agents = await store.get_all_agents()
36
+
37
+ if not agents:
38
+ console.print("[yellow]No custom agents found[/yellow]")
39
+ return
40
+
41
+ table = Table(title="Custom Agents")
42
+ table.add_column("ID", style="cyan")
43
+ table.add_column("Name", style="green")
44
+ table.add_column("Type", style="blue")
45
+ table.add_column("Status", style="yellow")
46
+
47
+ for agent_id, agent in agents.items():
48
+ table.add_row(agent_id, agent.name, agent.agent_type, "Active")
49
+
50
+ console.print(table)
51
+
52
+ asyncio.run(_run())
53
+
54
+
55
+ @app.command()
56
+ def create(
57
+ name: str = typer.Argument(..., help="Agent name"),
58
+ agent_type: str = typer.Option("detector", help="Agent type"),
59
+ description: str = typer.Option("", help="Agent description"),
60
+ project_dir: Optional[Path] = typer.Option(None, help="Project directory"),
61
+ ):
62
+ """Create a new custom agent."""
63
+
64
+ async def _run():
65
+ nonlocal project_dir
66
+ if project_dir is None:
67
+ project_dir = Path.cwd()
68
+
69
+ # Get template
70
+ template = get_agent_template(agent_type)
71
+ if not template:
72
+ console.print(f"[red]Unknown agent type: {agent_type}[/red]")
73
+ raise typer.Exit(1)
74
+
75
+ # Build custom agent
76
+ builder = AgentBuilder()
77
+ agent = builder.build(
78
+ name=name,
79
+ agent_type=agent_type,
80
+ description=description or template.get("description", ""),
81
+ config=template.get("config", {}),
82
+ )
83
+
84
+ # Store it
85
+ storage_path = project_dir / ".devloop" / "custom_agents"
86
+ store = CustomAgentStore(storage_path)
87
+ await store.save_agent(agent)
88
+
89
+ console.print(f"[green]✓[/green] Created custom agent: [cyan]{name}[/cyan]")
90
+ console.print(f" ID: {agent.id}")
91
+ console.print(f" Type: {agent_type}")
92
+
93
+ asyncio.run(_run())
94
+
95
+
96
+ @app.command()
97
+ def delete(
98
+ agent_id: str = typer.Argument(..., help="Agent ID"),
99
+ project_dir: Optional[Path] = typer.Option(None, help="Project directory"),
100
+ ):
101
+ """Delete a custom agent."""
102
+
103
+ async def _run():
104
+ nonlocal project_dir
105
+ if project_dir is None:
106
+ project_dir = Path.cwd()
107
+
108
+ storage_path = project_dir / ".devloop" / "custom_agents"
109
+ store = CustomAgentStore(storage_path)
110
+
111
+ await store.delete_agent(agent_id)
112
+ console.print(f"[green]✓[/green] Deleted agent: [cyan]{agent_id}[/cyan]")
113
+
114
+ asyncio.run(_run())
115
+
116
+
117
+ @app.command()
118
+ def templates():
119
+ """Show available agent templates."""
120
+ templates = {
121
+ "detector": {
122
+ "description": "Pattern detection agent",
123
+ "triggers": ["file:modified"],
124
+ },
125
+ "analyzer": {
126
+ "description": "Code analysis agent",
127
+ "triggers": ["file:modified"],
128
+ },
129
+ "generator": {
130
+ "description": "Code generation agent",
131
+ "triggers": ["command:generate"],
132
+ },
133
+ }
134
+
135
+ table = Table(title="Available Agent Templates")
136
+ table.add_column("Type", style="cyan")
137
+ table.add_column("Description", style="green")
138
+ table.add_column("Default Triggers", style="yellow")
139
+
140
+ for template_type, template_data in templates.items():
141
+ triggers = ", ".join(template_data["triggers"])
142
+ table.add_row(template_type, template_data["description"], triggers)
143
+
144
+ console.print(table)
@@ -0,0 +1,161 @@
1
+ """CLI commands for agent feedback and performance monitoring."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from devloop.core.feedback import FeedbackAPI, FeedbackStore
12
+ from devloop.core.performance import PerformanceMonitor
13
+
14
+ app = typer.Typer(help="Feedback and performance monitoring")
15
+ console = Console()
16
+
17
+
18
+ def get_feedback_api(project_dir: Path = None) -> FeedbackAPI:
19
+ """Get feedback API instance for the project."""
20
+ if project_dir is None:
21
+ project_dir = Path.cwd()
22
+
23
+ storage_path = project_dir / ".devloop" / "feedback"
24
+ feedback_store = FeedbackStore(storage_path)
25
+ return FeedbackAPI(feedback_store)
26
+
27
+
28
+ def get_performance_monitor(project_dir: Path = None) -> PerformanceMonitor:
29
+ """Get performance monitor instance for the project."""
30
+ if project_dir is None:
31
+ project_dir = Path.cwd()
32
+
33
+ storage_path = project_dir / ".devloop" / "performance"
34
+ return PerformanceMonitor(storage_path)
35
+
36
+
37
+ @app.command()
38
+ def list_feedback(
39
+ agent: Optional[str] = typer.Option(None, help="Filter by agent name"),
40
+ limit: int = typer.Option(20, help="Maximum number of feedback items to show"),
41
+ project_dir: Optional[Path] = typer.Option(None, help="Project directory"),
42
+ ):
43
+ """List recent feedback for agents."""
44
+
45
+ async def _run():
46
+ feedback_api = get_feedback_api(project_dir)
47
+
48
+ if agent:
49
+ # Get feedback for specific agent
50
+ feedback_items = await feedback_api.feedback_store.get_feedback_for_agent(
51
+ agent
52
+ )
53
+ else:
54
+ # Get all feedback
55
+ feedback_items = await feedback_api.feedback_store.get_all_feedback()
56
+
57
+ feedback_items = feedback_items[-limit:] # Get last N items
58
+
59
+ if not feedback_items:
60
+ console.print("[yellow]No feedback found[/yellow]")
61
+ return
62
+
63
+ table = Table(title="Agent Feedback")
64
+ table.add_column("Timestamp", style="cyan")
65
+ table.add_column("Agent", style="green")
66
+ table.add_column("Type", style="blue")
67
+ table.add_column("Message", style="yellow")
68
+
69
+ for item in feedback_items:
70
+ table.add_row(
71
+ item.timestamp.isoformat(),
72
+ item.agent_name,
73
+ item.feedback_type,
74
+ item.message[:50] + "..." if len(item.message) > 50 else item.message,
75
+ )
76
+
77
+ console.print(table)
78
+
79
+ asyncio.run(_run())
80
+
81
+
82
+ @app.command()
83
+ def submit_feedback(
84
+ agent: str = typer.Option(..., help="Agent name"),
85
+ feedback_type: str = typer.Option(
86
+ "suggestion", help="Feedback type: suggestion|bug|improvement|other"
87
+ ),
88
+ message: str = typer.Argument(..., help="Feedback message"),
89
+ project_dir: Optional[Path] = typer.Option(None, help="Project directory"),
90
+ ):
91
+ """Submit feedback for an agent."""
92
+
93
+ async def _run():
94
+ feedback_api = get_feedback_api(project_dir)
95
+
96
+ await feedback_api.submit_feedback(agent, feedback_type, message)
97
+ console.print(f"[green]✓[/green] Feedback submitted for [cyan]{agent}[/cyan]")
98
+
99
+ asyncio.run(_run())
100
+
101
+
102
+ @app.command()
103
+ def performance_status(
104
+ project_dir: Optional[Path] = typer.Option(None, help="Project directory"),
105
+ ):
106
+ """Show agent performance metrics."""
107
+
108
+ async def _run():
109
+ monitor = get_performance_monitor(project_dir)
110
+
111
+ metrics = await monitor.get_performance_summary()
112
+
113
+ table = Table(title="Agent Performance Metrics")
114
+ table.add_column("Agent", style="cyan")
115
+ table.add_column("Success Rate", style="green")
116
+ table.add_column("Avg Duration (ms)", style="blue")
117
+ table.add_column("Total Runs", style="yellow")
118
+
119
+ for agent_name, data in metrics.items():
120
+ success_rate = (
121
+ f"{data.get('success_rate', 0):.1%}"
122
+ if data.get("success_rate") is not None
123
+ else "N/A"
124
+ )
125
+ avg_duration = (
126
+ f"{data.get('avg_duration', 0):.2f}"
127
+ if data.get("avg_duration") is not None
128
+ else "N/A"
129
+ )
130
+ total_runs = data.get("total_runs", 0)
131
+
132
+ table.add_row(agent_name, success_rate, avg_duration, str(total_runs))
133
+
134
+ console.print(table)
135
+
136
+ asyncio.run(_run())
137
+
138
+
139
+ @app.command()
140
+ def performance_detail(
141
+ agent: str = typer.Option(..., help="Agent name"),
142
+ project_dir: Optional[Path] = typer.Option(None, help="Project directory"),
143
+ ):
144
+ """Show detailed performance metrics for an agent."""
145
+
146
+ async def _run():
147
+ monitor = get_performance_monitor(project_dir)
148
+
149
+ metrics = await monitor.get_agent_metrics(agent)
150
+
151
+ if not metrics:
152
+ console.print(f"[yellow]No metrics found for agent: {agent}[/yellow]")
153
+ return
154
+
155
+ console.print(f"\n[bold]Performance Metrics for {agent}[/bold]")
156
+ console.print(f" Success Rate: {metrics.get('success_rate', 0):.1%}")
157
+ console.print(f" Average Duration: {metrics.get('avg_duration', 0):.2f}ms")
158
+ console.print(f" Total Runs: {metrics.get('total_runs', 0)}")
159
+ console.print(f" Last Run: {metrics.get('last_run', 'Never')}")
160
+
161
+ asyncio.run(_run())
@@ -0,0 +1,50 @@
1
+ """Agent summary CLI command."""
2
+
3
+ import asyncio
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from devloop.core.context_store import context_store
10
+ from devloop.core.summary_generator import SummaryGenerator
11
+ from devloop.core.summary_formatter import SummaryFormatter
12
+
13
+ app = typer.Typer()
14
+ console = Console()
15
+
16
+
17
+ # Summary formatting is now handled by SummaryFormatter
18
+
19
+
20
+ @app.command()
21
+ def agent_summary(
22
+ scope: str = typer.Argument(
23
+ "recent", help="Summary scope: recent|today|session|all"
24
+ ),
25
+ agent: Optional[str] = typer.Option(None, "--agent", help="Filter by agent name"),
26
+ severity: Optional[str] = typer.Option(
27
+ None, "--severity", help="Filter by severity"
28
+ ),
29
+ category: Optional[str] = typer.Option(
30
+ None, "--category", help="Filter by category"
31
+ ),
32
+ ):
33
+ """Generate intelligent summary of dev-agent findings."""
34
+ filters = {}
35
+ if agent and agent is not None:
36
+ filters["agent"] = str(agent)
37
+ if severity and severity is not None:
38
+ filters["severity"] = str(severity)
39
+ if category and category is not None:
40
+ filters["category"] = str(category)
41
+
42
+ generator = SummaryGenerator(context_store)
43
+
44
+ try:
45
+ report = asyncio.run(generator.generate_summary(scope, filters))
46
+ markdown_output = SummaryFormatter.format_markdown(report)
47
+ console.print(markdown_output)
48
+ except Exception as e:
49
+ console.print(f"[red]Error generating summary: {e}[/red]")
50
+ raise typer.Exit(1)