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,302 @@
1
+ """Proactive feedback collection at natural development breakpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Any, Dict, List, Optional, Callable
9
+
10
+ from .event import Event, EventBus
11
+ from .feedback import FeedbackAPI, FeedbackType
12
+
13
+
14
+ @dataclass
15
+ class FeedbackPrompt:
16
+ """Represents a proactive feedback prompt."""
17
+
18
+ id: str
19
+ agent_name: str
20
+ event_type: str
21
+ prompt_type: str # 'quick_rating', 'detailed_feedback', 'thumbs_only'
22
+ message: str
23
+ context: Dict[str, Any]
24
+ timestamp: float
25
+ expires_at: float
26
+ callback: Optional[Callable] = None
27
+
28
+ def is_expired(self) -> bool:
29
+ return time.time() > self.expires_at
30
+
31
+
32
+ class ProactiveFeedbackManager:
33
+ """Manages proactive feedback prompts at natural development breakpoints."""
34
+
35
+ def __init__(self, event_bus: EventBus, feedback_api: FeedbackAPI):
36
+ self.event_bus = event_bus
37
+ self.feedback_api = feedback_api
38
+
39
+ # Active prompts waiting for developer response
40
+ self.active_prompts: Dict[str, FeedbackPrompt] = {}
41
+
42
+ # Prompt timing configuration
43
+ self.prompt_delays = {
44
+ "after_agent_success": 5, # 5 seconds after successful agent action
45
+ "after_agent_failure": 2, # 2 seconds after failed agent action
46
+ "after_file_save": 3, # 3 seconds after file save
47
+ "after_build_success": 10, # 10 seconds after successful build
48
+ "after_build_failure": 5, # 5 seconds after build failure
49
+ "idle_period": 300, # 5 minutes of inactivity
50
+ }
51
+
52
+ # Prompt expiration times
53
+ self.prompt_lifetimes = {
54
+ "quick_rating": 60, # 1 minute for quick ratings
55
+ "thumbs_only": 120, # 2 minutes for thumbs up/down
56
+ "detailed_feedback": 300, # 5 minutes for detailed feedback
57
+ }
58
+
59
+ self._setup_event_listeners()
60
+
61
+ def _setup_event_listeners(self):
62
+ """Set up listeners for development events that trigger feedback prompts."""
63
+ # Agent completion events
64
+ asyncio.create_task(
65
+ self.event_bus.subscribe("agent:*:completed", self._on_agent_completed)
66
+ )
67
+
68
+ # File system events
69
+ asyncio.create_task(self.event_bus.subscribe("file:saved", self._on_file_saved))
70
+ asyncio.create_task(
71
+ self.event_bus.subscribe("file:modified", self._on_file_modified)
72
+ )
73
+
74
+ # Build/test events (could be extended)
75
+ asyncio.create_task(
76
+ self.event_bus.subscribe("build:success", self._on_build_success)
77
+ )
78
+ asyncio.create_task(
79
+ self.event_bus.subscribe("build:failure", self._on_build_failure)
80
+ )
81
+
82
+ # Git events
83
+ asyncio.create_task(self.event_bus.subscribe("git:commit", self._on_git_commit))
84
+
85
+ async def _on_agent_completed(self, event: Event) -> None:
86
+ """Handle agent completion and schedule feedback prompt."""
87
+ agent_name = event.payload.get("agent_name")
88
+ success = event.payload.get("success", False)
89
+
90
+ if agent_name and success:
91
+ await self._schedule_prompt(
92
+ agent_name=agent_name,
93
+ event_type="agent:completed",
94
+ prompt_type="quick_rating",
95
+ message=f"How was the {agent_name} agent's recent action?",
96
+ context=event.payload,
97
+ delay_seconds=self.prompt_delays["after_agent_success"],
98
+ )
99
+ elif agent_name:
100
+ # For failures, ask for feedback more urgently
101
+ await self._schedule_prompt(
102
+ agent_name=agent_name,
103
+ event_type="agent:completed",
104
+ prompt_type="thumbs_only",
105
+ message=f"{agent_name} encountered an issue. Was this expected?",
106
+ context=event.payload,
107
+ delay_seconds=self.prompt_delays["after_agent_failure"],
108
+ )
109
+
110
+ async def _on_file_saved(self, event: Event) -> None:
111
+ """Handle file save events."""
112
+ # Only prompt occasionally to avoid being annoying
113
+ if asyncio.get_event_loop().time() % 10 < 1: # ~10% of the time
114
+ await self._schedule_prompt(
115
+ agent_name="filesystem", # Generic agent for file operations
116
+ event_type="file:saved",
117
+ prompt_type="thumbs_only",
118
+ message="How are you finding the file monitoring features?",
119
+ context=event.payload,
120
+ delay_seconds=self.prompt_delays["after_file_save"],
121
+ )
122
+
123
+ async def _on_file_modified(self, event: Event) -> None:
124
+ """Handle file modification events."""
125
+ # Less frequent prompts for modifications
126
+ pass # Could be implemented for specific scenarios
127
+
128
+ async def _on_build_success(self, event: Event) -> None:
129
+ """Handle successful build events."""
130
+ await self._schedule_prompt(
131
+ agent_name="build_system",
132
+ event_type="build:success",
133
+ prompt_type="quick_rating",
134
+ message="Build completed successfully! How did the automated checks perform?",
135
+ context=event.payload,
136
+ delay_seconds=self.prompt_delays["after_build_success"],
137
+ )
138
+
139
+ async def _on_build_failure(self, event: Event) -> None:
140
+ """Handle build failure events."""
141
+ await self._schedule_prompt(
142
+ agent_name="build_system",
143
+ event_type="build:failure",
144
+ prompt_type="detailed_feedback",
145
+ message="Build failed. How can we improve the error detection and reporting?",
146
+ context=event.payload,
147
+ delay_seconds=self.prompt_delays["after_build_failure"],
148
+ )
149
+
150
+ async def _on_git_commit(self, event: Event) -> None:
151
+ """Handle git commit events."""
152
+ # Occasional feedback about the overall development experience
153
+ if len(self.active_prompts) < 2: # Don't overwhelm with too many prompts
154
+ await self._schedule_prompt(
155
+ agent_name="development_workflow",
156
+ event_type="git:commit",
157
+ prompt_type="quick_rating",
158
+ message="How is your development workflow going?",
159
+ context=event.payload,
160
+ delay_seconds=1, # Immediate for commits
161
+ )
162
+
163
+ async def _schedule_prompt(
164
+ self,
165
+ agent_name: str,
166
+ event_type: str,
167
+ prompt_type: str,
168
+ message: str,
169
+ context: Dict[str, Any],
170
+ delay_seconds: int,
171
+ ) -> None:
172
+ """Schedule a feedback prompt to be shown after a delay."""
173
+ prompt_id = f"{agent_name}_{event_type}_{int(time.time())}"
174
+
175
+ lifetime = self.prompt_lifetimes.get(prompt_type, 60)
176
+ expires_at = time.time() + delay_seconds + lifetime
177
+
178
+ prompt = FeedbackPrompt(
179
+ id=prompt_id,
180
+ agent_name=agent_name,
181
+ event_type=event_type,
182
+ prompt_type=prompt_type,
183
+ message=message,
184
+ context=context,
185
+ timestamp=time.time() + delay_seconds,
186
+ expires_at=expires_at,
187
+ )
188
+
189
+ self.active_prompts[prompt_id] = prompt
190
+
191
+ # Schedule the prompt display
192
+ asyncio.create_task(self._show_prompt_after_delay(prompt, delay_seconds))
193
+
194
+ async def _show_prompt_after_delay(
195
+ self, prompt: FeedbackPrompt, delay: int
196
+ ) -> None:
197
+ """Show the feedback prompt after the specified delay."""
198
+ await asyncio.sleep(delay)
199
+
200
+ # Check if prompt is still active and not expired
201
+ if prompt.id in self.active_prompts and not prompt.is_expired():
202
+ await self._display_feedback_prompt(prompt)
203
+
204
+ async def _display_feedback_prompt(self, prompt: FeedbackPrompt) -> None:
205
+ """Display the feedback prompt to the developer."""
206
+ # In a real implementation, this would integrate with the IDE or terminal UI
207
+ # For now, we'll emit an event that can be caught by UI components
208
+
209
+ await self.event_bus.emit(
210
+ Event(
211
+ type="feedback:prompt",
212
+ payload={
213
+ "prompt_id": prompt.id,
214
+ "agent_name": prompt.agent_name,
215
+ "prompt_type": prompt.prompt_type,
216
+ "message": prompt.message,
217
+ "context": prompt.context,
218
+ "expires_at": prompt.expires_at,
219
+ },
220
+ source="proactive_feedback",
221
+ )
222
+ )
223
+
224
+ # Set up a timeout to auto-dismiss the prompt
225
+ asyncio.create_task(self._auto_dismiss_prompt(prompt.id, prompt.expires_at))
226
+
227
+ async def _auto_dismiss_prompt(self, prompt_id: str, expires_at: float) -> None:
228
+ """Auto-dismiss a prompt when it expires."""
229
+ remaining_time = expires_at - time.time()
230
+ if remaining_time > 0:
231
+ await asyncio.sleep(remaining_time)
232
+
233
+ if prompt_id in self.active_prompts:
234
+ del self.active_prompts[prompt_id]
235
+
236
+ async def submit_prompt_feedback(
237
+ self,
238
+ prompt_id: str,
239
+ feedback_type: FeedbackType,
240
+ value: Any,
241
+ comment: Optional[str] = None,
242
+ ) -> bool:
243
+ """Submit feedback for a proactive prompt."""
244
+ if prompt_id not in self.active_prompts:
245
+ return False
246
+
247
+ prompt = self.active_prompts[prompt_id]
248
+
249
+ # Submit the feedback
250
+ await self.feedback_api.submit_feedback(
251
+ agent_name=prompt.agent_name,
252
+ event_type=f"proactive_{prompt.event_type}",
253
+ feedback_type=feedback_type,
254
+ value=value,
255
+ comment=comment,
256
+ context={
257
+ "prompt_id": prompt_id,
258
+ "original_context": prompt.context,
259
+ "feedback_source": "proactive_prompt",
260
+ },
261
+ )
262
+
263
+ # Remove the prompt
264
+ del self.active_prompts[prompt_id]
265
+ return True
266
+
267
+ def get_active_prompts(self) -> List[Dict[str, Any]]:
268
+ """Get list of currently active feedback prompts."""
269
+ current_time = time.time()
270
+ active = []
271
+
272
+ for prompt in self.active_prompts.values():
273
+ if not prompt.is_expired():
274
+ active.append(
275
+ {
276
+ "id": prompt.id,
277
+ "agent_name": prompt.agent_name,
278
+ "message": prompt.message,
279
+ "prompt_type": prompt.prompt_type,
280
+ "time_remaining": max(0, prompt.expires_at - current_time),
281
+ }
282
+ )
283
+
284
+ return active
285
+
286
+ async def dismiss_prompt(self, prompt_id: str) -> bool:
287
+ """Dismiss a feedback prompt without submitting feedback."""
288
+ if prompt_id in self.active_prompts:
289
+ del self.active_prompts[prompt_id]
290
+ return True
291
+ return False
292
+
293
+ def cleanup_expired_prompts(self) -> int:
294
+ """Clean up expired prompts and return count removed."""
295
+ expired_ids = [
296
+ pid for pid, prompt in self.active_prompts.items() if prompt.is_expired()
297
+ ]
298
+
299
+ for pid in expired_ids:
300
+ del self.active_prompts[pid]
301
+
302
+ return len(expired_ids)
@@ -0,0 +1,159 @@
1
+ """Format summary reports for different outputs."""
2
+
3
+ from typing import Dict, Any
4
+ from .summary_generator import SummaryReport
5
+
6
+
7
+ class SummaryFormatter:
8
+ """Format summary reports for different output formats."""
9
+
10
+ @staticmethod
11
+ def format_markdown(report: SummaryReport) -> str:
12
+ """Format summary report as markdown."""
13
+ lines = []
14
+
15
+ # Header with emoji based on findings
16
+ if report.total_findings == 0:
17
+ emoji = "✅"
18
+ status = "All Clear"
19
+ elif report.critical_issues:
20
+ emoji = "🚨"
21
+ status = f"{len(report.critical_issues)} Critical Issues"
22
+ else:
23
+ emoji = "🔍"
24
+ status = "Findings Summary"
25
+
26
+ lines.append(f"## {emoji} DevLoop Summary ({status})")
27
+ lines.append(f"**Scope:** {report.scope.title()}")
28
+ lines.append(
29
+ f"**Time Range:** {report.time_range[0].strftime('%Y-%m-%d %H:%M')} - {report.time_range[1].strftime('%Y-%m-%d %H:%M')}"
30
+ )
31
+ lines.append("")
32
+
33
+ # Quick stats
34
+ lines.append("### 📊 Quick Stats")
35
+ lines.append(f"- **Total Findings:** {report.total_findings}")
36
+
37
+ if report.by_severity:
38
+ severity_parts = []
39
+ for severity in ["error", "warning", "info", "style"]:
40
+ count = report.by_severity.get(severity, 0)
41
+ if count > 0:
42
+ severity_parts.append(f"{count} {severity}")
43
+ if severity_parts:
44
+ lines.append(f"- **By Severity:** {', '.join(severity_parts)}")
45
+
46
+ if report.critical_issues:
47
+ lines.append(f"- **Critical Issues:** {len(report.critical_issues)}")
48
+
49
+ if report.auto_fixable:
50
+ lines.append(f"- **Auto-fixable:** {len(report.auto_fixable)}")
51
+
52
+ # Show trend if available
53
+ if "direction" in report.trends:
54
+ trend_emoji = {"improving": "📈", "worsening": "📉", "stable": "➡️"}.get(
55
+ report.trends["direction"], "➡️"
56
+ )
57
+ lines.append(
58
+ f"- **Trend:** {trend_emoji} {report.trends['direction'].title()}"
59
+ )
60
+ lines.append("")
61
+
62
+ # Agent breakdown
63
+ if report.by_agent:
64
+ lines.append("### 📈 Agent Performance")
65
+ for agent_name, summary in report.by_agent.items():
66
+ severity_str = ", ".join(
67
+ f"{count} {sev}"
68
+ for sev, count in summary.severity_breakdown.items()
69
+ )
70
+ lines.append(
71
+ f"- **{agent_name}:** {summary.finding_count} findings ({severity_str})"
72
+ )
73
+ lines.append("")
74
+
75
+ # Critical issues (top priority)
76
+ if report.critical_issues:
77
+ lines.append("### 🚨 Priority Issues")
78
+ for i, finding in enumerate(report.critical_issues[:5], 1): # Top 5
79
+ location = f"{finding.file}:{finding.line or '?'}"
80
+ message = finding.message[:100] + (
81
+ "..." if len(finding.message) > 100 else ""
82
+ )
83
+ lines.append(
84
+ f"{i}. **{finding.severity.value.title()}** in `{location}` - {message}"
85
+ )
86
+ lines.append("")
87
+
88
+ # Auto-fixable items
89
+ if report.auto_fixable and len(report.auto_fixable) > len(
90
+ report.critical_issues
91
+ ):
92
+ non_critical_auto_fixable = [
93
+ f for f in report.auto_fixable if f not in report.critical_issues
94
+ ]
95
+ if non_critical_auto_fixable:
96
+ lines.append("### 🔧 Auto-fixable Issues")
97
+ for i, finding in enumerate(non_critical_auto_fixable[:3], 1): # Top 3
98
+ location = f"{finding.file}:{finding.line or '?'}"
99
+ message = finding.message[:80] + (
100
+ "..." if len(finding.message) > 80 else ""
101
+ )
102
+ lines.append(f"{i}. `{location}` - {message}")
103
+ lines.append("")
104
+
105
+ # Insights
106
+ if report.insights:
107
+ lines.append("### 💡 Insights")
108
+ for insight in report.insights:
109
+ lines.append(f"- {insight}")
110
+ lines.append("")
111
+
112
+ # Quick actions
113
+ if report.auto_fixable:
114
+ lines.append("### 🛠️ Quick Actions")
115
+ lines.append(
116
+ f"Run `devloop auto-fix` to apply {len(report.auto_fixable)} safe fixes automatically"
117
+ )
118
+ lines.append("")
119
+
120
+ return "\n".join(lines)
121
+
122
+ @staticmethod
123
+ def format_json(report: SummaryReport) -> Dict[str, Any]:
124
+ """Format summary report as JSON for API responses."""
125
+ return {
126
+ "summary": {
127
+ "scope": report.scope,
128
+ "total_findings": report.total_findings,
129
+ "critical_count": len(report.critical_issues),
130
+ "auto_fixable_count": len(report.auto_fixable),
131
+ "trend": report.trends.get("direction", "stable"),
132
+ "trend_percentage": report.trends.get("change_percent", 0.0),
133
+ },
134
+ "by_agent": {
135
+ agent_name: {
136
+ "count": summary.finding_count,
137
+ "critical": sum(
138
+ 1
139
+ for f in summary.top_issues
140
+ if f.severity.value == "error" or f.blocking
141
+ ),
142
+ "auto_fixable": sum(
143
+ 1 for f in summary.top_issues if f.auto_fixable
144
+ ),
145
+ }
146
+ for agent_name, summary in report.by_agent.items()
147
+ },
148
+ "insights": report.insights,
149
+ "critical_issues": [
150
+ {
151
+ "file": issue.file,
152
+ "line": issue.line,
153
+ "severity": issue.severity.value,
154
+ "message": issue.message,
155
+ "agent": issue.agent,
156
+ }
157
+ for issue in report.critical_issues[:5]
158
+ ],
159
+ }