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,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
|
+
}
|