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,421 @@
1
+ #!/usr/bin/env python3
2
+ """Git Commit Message Assistant - Generates conventional commit messages."""
3
+
4
+ import asyncio
5
+ from pathlib import Path
6
+ from typing import Dict, Any, List, Optional
7
+ from dataclasses import dataclass
8
+
9
+ from ..core.agent import Agent, AgentResult
10
+ from ..core.context_store import context_store, Finding
11
+ from ..core.event import Event
12
+
13
+
14
+ @dataclass
15
+ class CommitConfig:
16
+ """Configuration for commit message generation."""
17
+
18
+ conventional_commits: bool = True
19
+ max_message_length: int = 72
20
+ include_breaking_changes: bool = True
21
+ analyze_file_changes: bool = True
22
+ auto_generate_scope: bool = True
23
+ common_types: Optional[List[str]] = None
24
+
25
+ def __post_init__(self):
26
+ if self.common_types is None:
27
+ self.common_types = [
28
+ "feat",
29
+ "fix",
30
+ "docs",
31
+ "style",
32
+ "refactor",
33
+ "test",
34
+ "chore",
35
+ "perf",
36
+ "ci",
37
+ "build",
38
+ ]
39
+
40
+
41
+ class CommitSuggestion:
42
+ """Commit message suggestion."""
43
+
44
+ def __init__(
45
+ self,
46
+ message: str,
47
+ confidence: float,
48
+ reasoning: str,
49
+ alternatives: List[str] = None,
50
+ ):
51
+ self.message = message
52
+ self.confidence = confidence
53
+ self.reasoning = reasoning
54
+ self.alternatives = alternatives or []
55
+
56
+ def to_dict(self) -> Dict[str, Any]:
57
+ return {
58
+ "message": self.message,
59
+ "confidence": self.confidence,
60
+ "reasoning": self.reasoning,
61
+ "alternatives": self.alternatives,
62
+ }
63
+
64
+
65
+ class GitCommitAssistantAgent(Agent):
66
+ """Agent for generating conventional commit messages."""
67
+
68
+ def __init__(self, config: Dict[str, Any], event_bus):
69
+ super().__init__(
70
+ "git-commit-assistant", ["git:pre-commit", "git:commit"], event_bus
71
+ )
72
+ self.config = CommitConfig(**config)
73
+
74
+ async def handle(self, event: Event) -> AgentResult:
75
+ """Handle git events by suggesting commit messages."""
76
+
77
+ event_type = event.type
78
+
79
+ if event_type == "git:pre-commit":
80
+ # Analyze staged changes and suggest commit message
81
+ return await self._handle_pre_commit(event)
82
+ elif event_type == "git:commit":
83
+ # Could validate commit message format
84
+ return await self._handle_commit(event)
85
+ else:
86
+ return AgentResult(
87
+ agent_name=self.name,
88
+ success=False,
89
+ duration=0.0,
90
+ message=f"Unsupported event type: {event_type}",
91
+ )
92
+
93
+ async def _handle_pre_commit(self, event: Event) -> AgentResult:
94
+ """Generate commit message suggestions for staged changes."""
95
+ try:
96
+ # Get staged files
97
+ staged_files = await self._get_staged_files()
98
+ if not staged_files:
99
+ return AgentResult(
100
+ agent_name=self.name,
101
+ success=True,
102
+ duration=0.0,
103
+ message="No staged files to analyze",
104
+ )
105
+
106
+ # Analyze changes
107
+ change_analysis = await self._analyze_changes(staged_files)
108
+
109
+ # Generate commit suggestions
110
+ suggestions = self._generate_commit_suggestions(change_analysis)
111
+
112
+ agent_result = AgentResult(
113
+ agent_name=self.name,
114
+ success=True,
115
+ duration=0.0,
116
+ message=f"Generated {len(suggestions)} commit message suggestions",
117
+ data={
118
+ "staged_files": staged_files,
119
+ "change_analysis": change_analysis,
120
+ "suggestions": [s.to_dict() for s in suggestions],
121
+ "top_suggestion": suggestions[0].to_dict() if suggestions else None,
122
+ },
123
+ )
124
+
125
+ # Write to context store for Claude Code integration
126
+ if suggestions:
127
+ top_suggestion = suggestions[0]
128
+ await context_store.add_finding(
129
+ Finding(
130
+ id=f"{self.name}-suggestion",
131
+ agent=self.name,
132
+ timestamp=str(event.timestamp),
133
+ file="",
134
+ message=f"Suggested commit message: {top_suggestion.message}",
135
+ suggestion=top_suggestion.message,
136
+ context=top_suggestion.to_dict(),
137
+ )
138
+ )
139
+
140
+ return agent_result
141
+
142
+ except Exception as e:
143
+ return AgentResult(
144
+ agent_name=self.name,
145
+ success=False,
146
+ duration=0.0,
147
+ message=f"Failed to generate commit suggestions: {str(e)}",
148
+ )
149
+
150
+ async def _handle_commit(self, event: Event) -> AgentResult:
151
+ """Validate commit message format."""
152
+ commit_msg = event.payload.get("message", "")
153
+ if not commit_msg:
154
+ return AgentResult(
155
+ agent_name=self.name,
156
+ success=False,
157
+ duration=0.0,
158
+ message="No commit message to validate",
159
+ )
160
+
161
+ is_valid, feedback = self._validate_commit_message(commit_msg)
162
+
163
+ return AgentResult(
164
+ agent_name=self.name,
165
+ success=is_valid,
166
+ duration=0.0,
167
+ message=f"Commit message validation: {'valid' if is_valid else 'invalid'}",
168
+ data={"message": commit_msg, "is_valid": is_valid, "feedback": feedback},
169
+ )
170
+
171
+ async def _get_staged_files(self) -> List[str]:
172
+ """Get list of staged files."""
173
+ try:
174
+ result = await asyncio.create_subprocess_exec(
175
+ "git",
176
+ "diff",
177
+ "--cached",
178
+ "--name-only",
179
+ stdout=asyncio.subprocess.PIPE,
180
+ stderr=asyncio.subprocess.PIPE,
181
+ )
182
+ stdout, _ = await result.communicate()
183
+
184
+ if result.returncode == 0:
185
+ files = stdout.decode().strip().split("\n")
186
+ return [f for f in files if f.strip()]
187
+ return []
188
+
189
+ except Exception:
190
+ return []
191
+
192
+ async def _analyze_changes(self, files: List[str]) -> Dict[str, Any]:
193
+ """Analyze the changes in staged files."""
194
+ analysis: Dict[str, Any] = {
195
+ "files_changed": len(files),
196
+ "file_types": {},
197
+ "change_types": [],
198
+ "affected_modules": set(),
199
+ "breaking_changes": False,
200
+ }
201
+
202
+ # Categorize files by type
203
+ for file in files:
204
+ path = Path(file)
205
+ ext = path.suffix
206
+
207
+ file_types = analysis["file_types"]
208
+ if ext == ".py":
209
+ file_types["python"] = file_types.get("python", 0) + 1
210
+ elif ext in [".js", ".ts", ".jsx", ".tsx"]:
211
+ file_types["javascript"] = file_types.get("javascript", 0) + 1
212
+ elif ext in [".md", ".rst", ".txt"]:
213
+ file_types["documentation"] = file_types.get("documentation", 0) + 1
214
+ elif ext in [".yml", ".yaml", ".json", ".toml"]:
215
+ file_types["config"] = file_types.get("config", 0) + 1
216
+ elif ext in [".sh", ".bat", ".ps1"]:
217
+ file_types["scripts"] = file_types.get("scripts", 0) + 1
218
+ else:
219
+ file_types["other"] = file_types.get("other", 0) + 1
220
+
221
+ # Extract module/area from path
222
+ parts = path.parts
223
+ if len(parts) > 1:
224
+ module = parts[0] if len(parts) > 1 else "root"
225
+ analysis["affected_modules"].add(module)
226
+
227
+ # Determine change types based on files
228
+ change_types = analysis["change_types"]
229
+ if analysis["file_types"].get("documentation", 0) > 0:
230
+ change_types.append("docs")
231
+ if analysis["file_types"].get("config", 0) > 0:
232
+ change_types.append("ci")
233
+ if analysis["file_types"].get("scripts", 0) > 0:
234
+ change_types.append("ci")
235
+
236
+ # Try to determine primary change type
237
+ if "test_" in " ".join(files).lower() or "spec" in " ".join(files).lower():
238
+ change_types.append("test")
239
+ elif any("fix" in f.lower() or "bug" in f.lower() for f in files):
240
+ change_types.append("fix")
241
+ elif any("feature" in f.lower() or "feat" in f.lower() for f in files):
242
+ change_types.append("feat")
243
+ else:
244
+ change_types.append("refactor") # Default assumption
245
+
246
+ analysis["affected_modules"] = list(analysis["affected_modules"])
247
+
248
+ return analysis
249
+
250
+ def _generate_commit_suggestions(
251
+ self, analysis: Dict[str, Any]
252
+ ) -> List[CommitSuggestion]:
253
+ """Generate commit message suggestions based on analysis."""
254
+ suggestions = []
255
+
256
+ # Determine primary type
257
+ primary_type = self._determine_primary_type(analysis)
258
+
259
+ # Generate scope
260
+ scope = self._generate_scope(analysis)
261
+
262
+ # Create main suggestion
263
+ message = f"{primary_type}"
264
+ if scope:
265
+ message += f"({scope})"
266
+
267
+ message += ": "
268
+
269
+ # Add description based on analysis
270
+ description = self._generate_description(analysis, primary_type)
271
+ message += description
272
+
273
+ # Ensure message length
274
+ if len(message) > self.config.max_message_length:
275
+ message = message[: self.config.max_message_length - 3] + "..."
276
+
277
+ suggestions.append(
278
+ CommitSuggestion(
279
+ message=message,
280
+ confidence=0.8,
281
+ reasoning=f"Based on {analysis['files_changed']} files changed, primarily {primary_type} changes",
282
+ )
283
+ )
284
+
285
+ # Generate alternative suggestions
286
+ alternatives = self._generate_alternatives(primary_type, scope, analysis)
287
+ suggestions.extend(alternatives)
288
+
289
+ return suggestions
290
+
291
+ def _determine_primary_type(self, analysis: Dict[str, Any]) -> str:
292
+ """Determine the primary commit type."""
293
+ change_types = analysis.get("change_types", [])
294
+
295
+ # Priority order for commit types
296
+ type_priority = {
297
+ "fix": ["fix", "bug", "patch"],
298
+ "feat": ["feat", "feature", "add"],
299
+ "docs": ["docs", "documentation"],
300
+ "test": ["test", "spec"],
301
+ "ci": ["ci", "config", "build"],
302
+ "refactor": ["refactor", "clean", "improve"],
303
+ "style": ["style", "format"],
304
+ "perf": ["perf", "performance", "optimize"],
305
+ "chore": ["chore", "maintenance"],
306
+ }
307
+
308
+ for commit_type, keywords in type_priority.items():
309
+ for change_type in change_types:
310
+ if any(keyword in change_type.lower() for keyword in keywords):
311
+ return commit_type
312
+
313
+ # Default to refactor if nothing specific
314
+ return "refactor"
315
+
316
+ def _generate_scope(self, analysis: Dict[str, Any]) -> str:
317
+ """Generate scope for conventional commit."""
318
+ if not self.config.auto_generate_scope:
319
+ return ""
320
+
321
+ modules = analysis.get("affected_modules", [])
322
+ if len(modules) == 1:
323
+ return modules[0].lower().replace(" ", "-")
324
+ elif len(modules) > 1:
325
+ # Find common prefix
326
+ common = ""
327
+ for i, char in enumerate(modules[0]):
328
+ if all(
329
+ module.startswith(modules[0][: i + 1]) for module in modules[1:]
330
+ ):
331
+ common = modules[0][: i + 1]
332
+ else:
333
+ break
334
+ return common.lower().replace(" ", "-") if common else ""
335
+
336
+ return ""
337
+
338
+ def _generate_description(self, analysis: Dict[str, Any], primary_type: str) -> str:
339
+ """Generate commit description."""
340
+ files_changed = analysis.get("files_changed", 0)
341
+
342
+ if primary_type == "docs":
343
+ return f"update documentation ({files_changed} files)"
344
+ elif primary_type == "test":
345
+ return f"add/update tests ({files_changed} files)"
346
+ elif primary_type == "ci":
347
+ return f"update CI/configuration ({files_changed} files)"
348
+ elif primary_type == "fix":
349
+ return f"fix issues in {files_changed} files"
350
+ elif primary_type == "feat":
351
+ return f"add new features ({files_changed} files)"
352
+ elif primary_type == "refactor":
353
+ return f"refactor code ({files_changed} files)"
354
+ elif primary_type == "style":
355
+ return f"improve code style ({files_changed} files)"
356
+ elif primary_type == "perf":
357
+ return f"improve performance ({files_changed} files)"
358
+ else:
359
+ return f"update {files_changed} files"
360
+
361
+ def _generate_alternatives(
362
+ self, primary_type: str, scope: str, analysis: Dict[str, Any]
363
+ ) -> List[CommitSuggestion]:
364
+ """Generate alternative commit message suggestions."""
365
+ alternatives = []
366
+
367
+ # Alternative with different wording
368
+ alt1 = f"{primary_type}"
369
+ if scope:
370
+ alt1 += f"({scope})"
371
+ alt1 += ": update code"
372
+
373
+ alternatives.append(
374
+ CommitSuggestion(
375
+ message=alt1, confidence=0.6, reasoning="Simple alternative message"
376
+ )
377
+ )
378
+
379
+ # Alternative without scope
380
+ alt2 = f"{primary_type}: {self._generate_description(analysis, primary_type)}"
381
+
382
+ alternatives.append(
383
+ CommitSuggestion(
384
+ message=alt2, confidence=0.5, reasoning="Message without scope"
385
+ )
386
+ )
387
+
388
+ return alternatives
389
+
390
+ def _validate_commit_message(self, message: str) -> tuple[bool, str]:
391
+ """Validate commit message format."""
392
+ if not self.config.conventional_commits:
393
+ return True, "Conventional commits not enforced"
394
+
395
+ # Basic conventional commit validation
396
+ if ":" not in message:
397
+ return False, "Missing ':' separator for conventional commit format"
398
+
399
+ type_part = message.split(":")[0].strip()
400
+
401
+ # Check if type is valid
402
+ if "(" in type_part and ")" in type_part:
403
+ # Has scope
404
+ type_only = type_part.split("(")[0]
405
+ else:
406
+ type_only = type_part
407
+
408
+ if self.config.common_types and type_only not in self.config.common_types:
409
+ return (
410
+ False,
411
+ f"Unknown commit type '{type_only}'. Use one of: {', '.join(self.config.common_types)}",
412
+ )
413
+
414
+ # Check length
415
+ if len(message) > 100: # Allow longer than subject line for full message
416
+ return (
417
+ False,
418
+ "Commit message too long (keep under 100 characters for subject)",
419
+ )
420
+
421
+ return True, "Valid conventional commit format"