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
|
@@ -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"
|