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,498 @@
|
|
|
1
|
+
"""Custom agent creation framework and templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import aiofiles
|
|
6
|
+
import importlib.util
|
|
7
|
+
import inspect
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from .agent import Agent
|
|
14
|
+
from .event import EventBus
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AgentTemplate:
|
|
19
|
+
"""Template for creating custom agents."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
description: str
|
|
23
|
+
category: str
|
|
24
|
+
triggers: List[str]
|
|
25
|
+
config_schema: Dict[str, Any]
|
|
26
|
+
template_code: str
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls, data: Dict[str, Any]) -> AgentTemplate:
|
|
30
|
+
"""Create template from dictionary."""
|
|
31
|
+
return cls(
|
|
32
|
+
name=data["name"],
|
|
33
|
+
description=data["description"],
|
|
34
|
+
category=data["category"],
|
|
35
|
+
triggers=data["triggers"],
|
|
36
|
+
config_schema=data["config_schema"],
|
|
37
|
+
template_code=data["template_code"],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AgentTemplateRegistry:
|
|
42
|
+
"""Registry of available agent templates."""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.templates: Dict[str, AgentTemplate] = {}
|
|
46
|
+
self._load_builtin_templates()
|
|
47
|
+
|
|
48
|
+
def _load_builtin_templates(self) -> None:
|
|
49
|
+
"""Load built-in agent templates."""
|
|
50
|
+
self.templates.update(
|
|
51
|
+
{
|
|
52
|
+
"file-watcher": AgentTemplate(
|
|
53
|
+
name="file-watcher",
|
|
54
|
+
description="Monitor specific file patterns and perform custom actions",
|
|
55
|
+
category="monitoring",
|
|
56
|
+
triggers=["file:modified", "file:created", "file:deleted"],
|
|
57
|
+
config_schema={
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"filePatterns": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"items": {"type": "string"},
|
|
63
|
+
"default": ["**/*.txt"],
|
|
64
|
+
},
|
|
65
|
+
"action": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"enum": ["log", "backup", "notify"],
|
|
68
|
+
"default": "log",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
template_code='''
|
|
73
|
+
from devloop.core.agent import Agent, AgentResult
|
|
74
|
+
|
|
75
|
+
class FileWatcherAgent(Agent):
|
|
76
|
+
def __init__(self, name: str, triggers: list, event_bus, config: dict):
|
|
77
|
+
super().__init__(name, triggers, event_bus)
|
|
78
|
+
self.config = config
|
|
79
|
+
|
|
80
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
81
|
+
file_path = event.payload.get("path", "")
|
|
82
|
+
action = self.config.get("action", "log")
|
|
83
|
+
|
|
84
|
+
# Check if file matches patterns
|
|
85
|
+
if not self._matches_pattern(file_path):
|
|
86
|
+
return AgentResult(
|
|
87
|
+
agent_name=self.name,
|
|
88
|
+
success=True,
|
|
89
|
+
duration=0.0,
|
|
90
|
+
message=f"File {file_path} doesn't match patterns"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if action == "log":
|
|
94
|
+
message = f"File {event.type.split(':')[1]}: {file_path}"
|
|
95
|
+
elif action == "backup":
|
|
96
|
+
# TODO: Implement backup logic
|
|
97
|
+
message = f"Backed up: {file_path}"
|
|
98
|
+
elif action == "notify":
|
|
99
|
+
# TODO: Implement notification logic
|
|
100
|
+
message = f"Notification sent for: {file_path}"
|
|
101
|
+
else:
|
|
102
|
+
message = f"Unknown action '{action}' for: {file_path}"
|
|
103
|
+
|
|
104
|
+
return AgentResult(
|
|
105
|
+
agent_name=self.name,
|
|
106
|
+
success=True,
|
|
107
|
+
duration=0.1,
|
|
108
|
+
message=message
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _matches_pattern(self, file_path: str) -> bool:
|
|
112
|
+
"""Check if file path matches configured patterns."""
|
|
113
|
+
from fnmatch import fnmatch
|
|
114
|
+
|
|
115
|
+
patterns = self.config.get("filePatterns", [])
|
|
116
|
+
for pattern in patterns:
|
|
117
|
+
if fnmatch(file_path, pattern):
|
|
118
|
+
return True
|
|
119
|
+
return False
|
|
120
|
+
''',
|
|
121
|
+
),
|
|
122
|
+
"command-runner": AgentTemplate(
|
|
123
|
+
name="command-runner",
|
|
124
|
+
description="Run shell commands in response to events",
|
|
125
|
+
category="automation",
|
|
126
|
+
triggers=["file:modified", "git:commit"],
|
|
127
|
+
config_schema={
|
|
128
|
+
"type": "object",
|
|
129
|
+
"properties": {
|
|
130
|
+
"commands": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"items": {"type": "string"},
|
|
133
|
+
"default": ["echo 'Hello from custom agent!'"],
|
|
134
|
+
},
|
|
135
|
+
"workingDirectory": {"type": "string", "default": "."},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
template_code="""
|
|
139
|
+
import subprocess
|
|
140
|
+
import asyncio
|
|
141
|
+
from devloop.core.agent import Agent, AgentResult
|
|
142
|
+
|
|
143
|
+
class CommandRunnerAgent(Agent):
|
|
144
|
+
def __init__(self, name: str, triggers: list, event_bus, config: dict):
|
|
145
|
+
super().__init__(name, triggers, event_bus)
|
|
146
|
+
self.config = config
|
|
147
|
+
|
|
148
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
149
|
+
commands = self.config.get("commands", [])
|
|
150
|
+
cwd = self.config.get("workingDirectory", ".")
|
|
151
|
+
|
|
152
|
+
results = []
|
|
153
|
+
for cmd in commands:
|
|
154
|
+
try:
|
|
155
|
+
# Run command asynchronously
|
|
156
|
+
process = await asyncio.create_subprocess_shell(
|
|
157
|
+
cmd,
|
|
158
|
+
stdout=asyncio.subprocess.PIPE,
|
|
159
|
+
stderr=asyncio.subprocess.PIPE,
|
|
160
|
+
cwd=cwd
|
|
161
|
+
)
|
|
162
|
+
stdout, stderr = await process.communicate()
|
|
163
|
+
|
|
164
|
+
if process.returncode == 0:
|
|
165
|
+
results.append(f"✓ {cmd}")
|
|
166
|
+
else:
|
|
167
|
+
results.append(f"✗ {cmd}: {stderr.decode().strip()}")
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
results.append(f"✗ {cmd}: {str(e)}")
|
|
171
|
+
|
|
172
|
+
return AgentResult(
|
|
173
|
+
agent_name=self.name,
|
|
174
|
+
success=all("✓" in r for r in results),
|
|
175
|
+
duration=0.1,
|
|
176
|
+
message=f"Ran {len(commands)} commands: {'; '.join(results)}"
|
|
177
|
+
)
|
|
178
|
+
""",
|
|
179
|
+
),
|
|
180
|
+
"data-processor": AgentTemplate(
|
|
181
|
+
name="data-processor",
|
|
182
|
+
description="Process and transform data files",
|
|
183
|
+
category="data",
|
|
184
|
+
triggers=["file:modified"],
|
|
185
|
+
config_schema={
|
|
186
|
+
"type": "object",
|
|
187
|
+
"properties": {
|
|
188
|
+
"inputFormat": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"enum": ["json", "csv", "txt"],
|
|
191
|
+
"default": "json",
|
|
192
|
+
},
|
|
193
|
+
"outputFormat": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"enum": ["json", "csv", "txt"],
|
|
196
|
+
"default": "json",
|
|
197
|
+
},
|
|
198
|
+
"transformations": {
|
|
199
|
+
"type": "array",
|
|
200
|
+
"items": {"type": "string"},
|
|
201
|
+
"default": [],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
template_code='''
|
|
206
|
+
import json
|
|
207
|
+
import csv
|
|
208
|
+
from pathlib import Path
|
|
209
|
+
from devloop.core.agent import Agent, AgentResult
|
|
210
|
+
|
|
211
|
+
class DataProcessorAgent(Agent):
|
|
212
|
+
def __init__(self, name: str, triggers: list, event_bus, config: dict):
|
|
213
|
+
super().__init__(name, triggers, event_bus)
|
|
214
|
+
self.config = config
|
|
215
|
+
|
|
216
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
217
|
+
file_path = event.payload.get("path", "")
|
|
218
|
+
if not file_path:
|
|
219
|
+
return AgentResult(
|
|
220
|
+
agent_name=self.name,
|
|
221
|
+
success=False,
|
|
222
|
+
duration=0.0,
|
|
223
|
+
message="No file path provided"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
path = Path(file_path)
|
|
227
|
+
if not path.exists():
|
|
228
|
+
return AgentResult(
|
|
229
|
+
agent_name=self.name,
|
|
230
|
+
success=False,
|
|
231
|
+
duration=0.0,
|
|
232
|
+
message=f"File does not exist: {file_path}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
# Read input data
|
|
237
|
+
input_format = self.config.get("inputFormat", "json")
|
|
238
|
+
data = await self._read_data(path, input_format)
|
|
239
|
+
|
|
240
|
+
# Apply transformations
|
|
241
|
+
transformations = self.config.get("transformations", [])
|
|
242
|
+
for transform in transformations:
|
|
243
|
+
data = await self._apply_transformation(data, transform)
|
|
244
|
+
|
|
245
|
+
# Write output data
|
|
246
|
+
output_format = self.config.get("outputFormat", "json")
|
|
247
|
+
output_path = path.with_suffix(f".processed{path.suffix}")
|
|
248
|
+
await self._write_data(output_path, data, output_format)
|
|
249
|
+
|
|
250
|
+
return AgentResult(
|
|
251
|
+
agent_name=self.name,
|
|
252
|
+
success=True,
|
|
253
|
+
duration=0.1,
|
|
254
|
+
message=f"Processed {file_path} -> {output_path}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
return AgentResult(
|
|
259
|
+
agent_name=self.name,
|
|
260
|
+
success=False,
|
|
261
|
+
duration=0.1,
|
|
262
|
+
message=f"Processing failed: {str(e)}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def _read_data(self, path: Path, format_type: str):
|
|
266
|
+
"""Read data from file."""
|
|
267
|
+
async with aiofiles.open(path, 'r') as f:
|
|
268
|
+
content = await f.read()
|
|
269
|
+
|
|
270
|
+
if format_type == "json":
|
|
271
|
+
return json.loads(content)
|
|
272
|
+
elif format_type == "csv":
|
|
273
|
+
import io
|
|
274
|
+
return list(csv.DictReader(io.StringIO(content)))
|
|
275
|
+
else: # txt
|
|
276
|
+
return content.splitlines()
|
|
277
|
+
|
|
278
|
+
async def _write_data(self, path: Path, data, format_type: str):
|
|
279
|
+
"""Write data to file."""
|
|
280
|
+
async with aiofiles.open(path, 'w') as f:
|
|
281
|
+
if format_type == "json":
|
|
282
|
+
await f.write(json.dumps(data, indent=2))
|
|
283
|
+
elif format_type == "csv":
|
|
284
|
+
if isinstance(data, list) and data:
|
|
285
|
+
writer = csv.DictWriter(f, fieldnames=data[0].keys())
|
|
286
|
+
writer.writeheader()
|
|
287
|
+
writer.writerows(data)
|
|
288
|
+
else: # txt
|
|
289
|
+
if isinstance(data, list):
|
|
290
|
+
await f.write('\\n'.join(data))
|
|
291
|
+
else:
|
|
292
|
+
await f.write(str(data))
|
|
293
|
+
|
|
294
|
+
async def _apply_transformation(self, data, transform: str):
|
|
295
|
+
"""Apply a transformation to data."""
|
|
296
|
+
# Simple transformation examples
|
|
297
|
+
if transform == "uppercase":
|
|
298
|
+
if isinstance(data, str):
|
|
299
|
+
return data.upper()
|
|
300
|
+
elif isinstance(data, list):
|
|
301
|
+
return [str(item).upper() for item in data]
|
|
302
|
+
elif transform == "lowercase":
|
|
303
|
+
if isinstance(data, str):
|
|
304
|
+
return data.lower()
|
|
305
|
+
elif isinstance(data, list):
|
|
306
|
+
return [str(item).lower() for item in data]
|
|
307
|
+
elif transform == "sort":
|
|
308
|
+
if isinstance(data, list):
|
|
309
|
+
return sorted(data, key=str)
|
|
310
|
+
# Add more transformations as needed
|
|
311
|
+
|
|
312
|
+
return data
|
|
313
|
+
''',
|
|
314
|
+
),
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def get_template(self, name: str) -> Optional[AgentTemplate]:
|
|
319
|
+
"""Get a template by name."""
|
|
320
|
+
return self.templates.get(name)
|
|
321
|
+
|
|
322
|
+
def list_templates(self, category: Optional[str] = None) -> List[AgentTemplate]:
|
|
323
|
+
"""List available templates, optionally filtered by category."""
|
|
324
|
+
templates = list(self.templates.values())
|
|
325
|
+
if category:
|
|
326
|
+
templates = [t for t in templates if t.category == category]
|
|
327
|
+
return templates
|
|
328
|
+
|
|
329
|
+
def get_categories(self) -> List[str]:
|
|
330
|
+
"""Get list of available template categories."""
|
|
331
|
+
return list(set(t.category for t in self.templates.values()))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class AgentFactory:
|
|
335
|
+
"""Factory for creating agents from templates and custom code."""
|
|
336
|
+
|
|
337
|
+
def __init__(self, template_registry: AgentTemplateRegistry):
|
|
338
|
+
self.template_registry = template_registry
|
|
339
|
+
|
|
340
|
+
async def create_from_template(
|
|
341
|
+
self,
|
|
342
|
+
template_name: str,
|
|
343
|
+
agent_name: str,
|
|
344
|
+
triggers: List[str],
|
|
345
|
+
event_bus: EventBus,
|
|
346
|
+
config: Dict[str, Any],
|
|
347
|
+
) -> Optional[Agent]:
|
|
348
|
+
"""Create an agent from a template."""
|
|
349
|
+
template = self.template_registry.get_template(template_name)
|
|
350
|
+
if not template:
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
# Create a temporary module with the template code
|
|
354
|
+
spec = importlib.util.spec_from_loader(
|
|
355
|
+
f"custom_agent_{agent_name}", loader=None
|
|
356
|
+
)
|
|
357
|
+
if spec is None:
|
|
358
|
+
return None
|
|
359
|
+
module = importlib.util.module_from_spec(spec)
|
|
360
|
+
|
|
361
|
+
# Execute the template code in the module
|
|
362
|
+
# SECURITY: exec() is used for dynamic agent loading from trusted templates
|
|
363
|
+
# This is an intentional design decision for the agent framework
|
|
364
|
+
exec(template.template_code, module.__dict__) # nosec B102
|
|
365
|
+
|
|
366
|
+
# Find the agent class (assume it's the first Agent subclass)
|
|
367
|
+
agent_class = None
|
|
368
|
+
for name, obj in module.__dict__.items():
|
|
369
|
+
if inspect.isclass(obj) and issubclass(obj, Agent) and obj != Agent:
|
|
370
|
+
agent_class = obj
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
if not agent_class:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
# Create and return the agent instance
|
|
377
|
+
return agent_class(
|
|
378
|
+
name=agent_name,
|
|
379
|
+
triggers=triggers,
|
|
380
|
+
event_bus=event_bus,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
async def create_from_file(
|
|
384
|
+
self,
|
|
385
|
+
file_path: Path,
|
|
386
|
+
agent_name: str,
|
|
387
|
+
triggers: List[str],
|
|
388
|
+
event_bus: EventBus,
|
|
389
|
+
config: Optional[Dict[str, Any]] = None,
|
|
390
|
+
) -> Optional[Agent]:
|
|
391
|
+
"""Create an agent from a Python file."""
|
|
392
|
+
if not file_path.exists():
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
# Load the module
|
|
396
|
+
spec = importlib.util.spec_from_file_location(
|
|
397
|
+
f"custom_agent_{agent_name}", file_path
|
|
398
|
+
)
|
|
399
|
+
if not spec or not spec.loader:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
module = importlib.util.module_from_spec(spec)
|
|
403
|
+
spec.loader.exec_module(module)
|
|
404
|
+
|
|
405
|
+
# Find the agent class
|
|
406
|
+
agent_class = None
|
|
407
|
+
for name, obj in module.__dict__.items():
|
|
408
|
+
if inspect.isclass(obj) and issubclass(obj, Agent) and obj != Agent:
|
|
409
|
+
agent_class = obj
|
|
410
|
+
break
|
|
411
|
+
|
|
412
|
+
if not agent_class:
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
# Create and return the agent instance
|
|
416
|
+
return agent_class(
|
|
417
|
+
name=agent_name,
|
|
418
|
+
triggers=triggers,
|
|
419
|
+
event_bus=event_bus,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class AgentMarketplace:
|
|
424
|
+
"""Marketplace for sharing and discovering custom agents."""
|
|
425
|
+
|
|
426
|
+
def __init__(self, marketplace_path: Path):
|
|
427
|
+
self.marketplace_path = marketplace_path
|
|
428
|
+
self.marketplace_path.mkdir(parents=True, exist_ok=True)
|
|
429
|
+
self.index_file = marketplace_path / "index.json"
|
|
430
|
+
self.agents_dir = marketplace_path / "agents"
|
|
431
|
+
|
|
432
|
+
async def publish_agent(self, agent_file: Path, metadata: Dict[str, Any]) -> bool:
|
|
433
|
+
"""Publish an agent to the marketplace."""
|
|
434
|
+
if not agent_file.exists():
|
|
435
|
+
return False
|
|
436
|
+
|
|
437
|
+
# Copy agent file to marketplace
|
|
438
|
+
agent_id = metadata.get("name", agent_file.stem)
|
|
439
|
+
agent_dir = self.agents_dir / agent_id
|
|
440
|
+
agent_dir.mkdir(exist_ok=True)
|
|
441
|
+
|
|
442
|
+
import shutil
|
|
443
|
+
|
|
444
|
+
shutil.copy2(agent_file, agent_dir / "agent.py")
|
|
445
|
+
|
|
446
|
+
# Save metadata
|
|
447
|
+
import time
|
|
448
|
+
|
|
449
|
+
metadata["published_at"] = metadata.get("published_at", time.time())
|
|
450
|
+
async with aiofiles.open(agent_dir / "metadata.json", "w") as f:
|
|
451
|
+
await f.write(json.dumps(metadata, indent=2))
|
|
452
|
+
|
|
453
|
+
# Update index
|
|
454
|
+
await self._update_index()
|
|
455
|
+
return True
|
|
456
|
+
|
|
457
|
+
async def download_agent(self, agent_id: str) -> Optional[Path]:
|
|
458
|
+
"""Download an agent from the marketplace."""
|
|
459
|
+
agent_dir = self.agents_dir / agent_id
|
|
460
|
+
if not agent_dir.exists():
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
return agent_dir / "agent.py"
|
|
464
|
+
|
|
465
|
+
async def list_agents(self, category: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
466
|
+
"""List available agents in the marketplace."""
|
|
467
|
+
if not self.index_file.exists():
|
|
468
|
+
return []
|
|
469
|
+
|
|
470
|
+
async with aiofiles.open(self.index_file, "r") as f:
|
|
471
|
+
content = await f.read()
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
agents = json.loads(content)
|
|
475
|
+
if category:
|
|
476
|
+
agents = [a for a in agents if a.get("category") == category]
|
|
477
|
+
return agents
|
|
478
|
+
except json.JSONDecodeError:
|
|
479
|
+
return []
|
|
480
|
+
|
|
481
|
+
async def _update_index(self) -> None:
|
|
482
|
+
"""Update the marketplace index."""
|
|
483
|
+
agents = []
|
|
484
|
+
|
|
485
|
+
for agent_dir in self.agents_dir.iterdir():
|
|
486
|
+
if agent_dir.is_dir():
|
|
487
|
+
metadata_file = agent_dir / "metadata.json"
|
|
488
|
+
if metadata_file.exists():
|
|
489
|
+
async with aiofiles.open(metadata_file, "r") as f:
|
|
490
|
+
try:
|
|
491
|
+
metadata = json.loads(await f.read())
|
|
492
|
+
metadata["id"] = agent_dir.name
|
|
493
|
+
agents.append(metadata)
|
|
494
|
+
except json.JSONDecodeError:
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
async with aiofiles.open(self.index_file, "w") as f:
|
|
498
|
+
await f.write(json.dumps(agents, indent=2))
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Integration commands for Amp/Claude Code."""
|
|
2
|
+
|
|
3
|
+
from devloop.core.auto_fix import apply_safe_fixes
|
|
4
|
+
from devloop.core.config import config
|
|
5
|
+
from devloop.core.context_store import context_store
|
|
6
|
+
from devloop.core.summary_generator import SummaryGenerator
|
|
7
|
+
from devloop.core.summary_formatter import SummaryFormatter
|
|
8
|
+
from typing import Any, Callable, Coroutine, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def check_agent_findings():
|
|
12
|
+
"""Check what findings agents have discovered."""
|
|
13
|
+
findings = await context_store.get_findings()
|
|
14
|
+
|
|
15
|
+
# Group findings by agent
|
|
16
|
+
findings_by_agent = {}
|
|
17
|
+
for finding in findings:
|
|
18
|
+
agent = finding.agent
|
|
19
|
+
if agent not in findings_by_agent:
|
|
20
|
+
findings_by_agent[agent] = []
|
|
21
|
+
findings_by_agent[agent].append(
|
|
22
|
+
{
|
|
23
|
+
"id": finding.id,
|
|
24
|
+
"file": finding.file,
|
|
25
|
+
"severity": finding.severity.value,
|
|
26
|
+
"message": finding.message,
|
|
27
|
+
"blocking": finding.blocking,
|
|
28
|
+
"auto_fixable": finding.auto_fixable,
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
summary = {
|
|
33
|
+
"total_findings": len(findings),
|
|
34
|
+
"findings_by_agent": {k: len(v) for k, v in findings_by_agent.items()},
|
|
35
|
+
"blockers": len([f for f in findings if f.blocking]),
|
|
36
|
+
"auto_fixable": len([f for f in findings if f.auto_fixable]),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"summary": summary,
|
|
41
|
+
"findings": findings_by_agent,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def apply_autonomous_fixes():
|
|
46
|
+
"""Apply safe fixes automatically."""
|
|
47
|
+
global_config = config.get_global_config()
|
|
48
|
+
|
|
49
|
+
if not global_config.autonomous_fixes.enabled:
|
|
50
|
+
return {
|
|
51
|
+
"message": "Autonomous fixes are disabled in configuration",
|
|
52
|
+
"applied_fixes": {},
|
|
53
|
+
"total_applied": 0,
|
|
54
|
+
"config_status": {
|
|
55
|
+
"enabled": False,
|
|
56
|
+
"safety_level": global_config.autonomous_fixes.safety_level,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
results = await apply_safe_fixes()
|
|
61
|
+
|
|
62
|
+
total_applied = sum(results.values())
|
|
63
|
+
message = f"Applied {total_applied} safe fixes"
|
|
64
|
+
|
|
65
|
+
if results:
|
|
66
|
+
details = []
|
|
67
|
+
for agent_type, count in results.items():
|
|
68
|
+
details.append(f"{count} {agent_type} fixes")
|
|
69
|
+
message += f": {', '.join(details)}"
|
|
70
|
+
else:
|
|
71
|
+
message += " (no safe fixes found)"
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"message": message,
|
|
75
|
+
"applied_fixes": results,
|
|
76
|
+
"total_applied": total_applied,
|
|
77
|
+
"config_status": {
|
|
78
|
+
"enabled": True,
|
|
79
|
+
"safety_level": global_config.autonomous_fixes.safety_level,
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def show_agent_status():
|
|
85
|
+
"""Show current status of background agents."""
|
|
86
|
+
findings = await context_store.get_findings()
|
|
87
|
+
|
|
88
|
+
# Group findings by agent
|
|
89
|
+
findings_by_agent = {}
|
|
90
|
+
for finding in findings:
|
|
91
|
+
agent = finding.agent
|
|
92
|
+
if agent not in findings_by_agent:
|
|
93
|
+
findings_by_agent[agent] = []
|
|
94
|
+
findings_by_agent[agent].append(finding)
|
|
95
|
+
|
|
96
|
+
status = {
|
|
97
|
+
"agent_activity": {},
|
|
98
|
+
"total_findings": len(findings),
|
|
99
|
+
"findings_by_agent": {k: len(v) for k, v in findings_by_agent.items()},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Show agent activity
|
|
103
|
+
for agent_type, agent_findings in findings_by_agent.items():
|
|
104
|
+
if agent_findings:
|
|
105
|
+
latest = max(agent_findings, key=lambda x: x.timestamp)
|
|
106
|
+
status["agent_activity"][agent_type] = {
|
|
107
|
+
"last_active": latest.timestamp,
|
|
108
|
+
"last_message": latest.message,
|
|
109
|
+
"total_findings": len(agent_findings),
|
|
110
|
+
"blocking_issues": len([f for f in agent_findings if f.blocking]),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Show recent findings (last 3 per agent)
|
|
114
|
+
status["recent_findings"] = {}
|
|
115
|
+
for agent_type, agent_findings in findings_by_agent.items():
|
|
116
|
+
recent = sorted(agent_findings, key=lambda x: x.timestamp, reverse=True)[:3]
|
|
117
|
+
status["recent_findings"][agent_type] = [
|
|
118
|
+
{
|
|
119
|
+
"timestamp": f.timestamp,
|
|
120
|
+
"message": f.message,
|
|
121
|
+
"severity": f.severity.value,
|
|
122
|
+
"blocking": f.blocking,
|
|
123
|
+
}
|
|
124
|
+
for f in recent
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
return status
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def generate_agent_summary(scope: str = "recent", filters: dict = None):
|
|
131
|
+
"""Generate agent summary report for Amp/Claude Code slash command."""
|
|
132
|
+
if filters is None:
|
|
133
|
+
filters = {}
|
|
134
|
+
|
|
135
|
+
generator = SummaryGenerator(context_store)
|
|
136
|
+
report = await generator.generate_summary(scope, filters)
|
|
137
|
+
|
|
138
|
+
formatter = SummaryFormatter()
|
|
139
|
+
return {
|
|
140
|
+
"summary": formatter.format_json(report),
|
|
141
|
+
"formatted_report": formatter.format_markdown(report),
|
|
142
|
+
"quick_stats": {
|
|
143
|
+
"total_findings": report.total_findings,
|
|
144
|
+
"critical_issues": len(report.critical_issues),
|
|
145
|
+
"auto_fixable": len(report.auto_fixable),
|
|
146
|
+
"trend": report.trends.get("direction", "stable"),
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Amp subagent command mappings
|
|
152
|
+
AMP_COMMANDS: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
|
|
153
|
+
"check_agent_findings": check_agent_findings,
|
|
154
|
+
"apply_autonomous_fixes": apply_autonomous_fixes,
|
|
155
|
+
"show_agent_status": show_agent_status,
|
|
156
|
+
"generate_agent_summary": generate_agent_summary,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def execute_amp_command(command: str, **kwargs) -> Any:
|
|
161
|
+
"""Execute an Amp integration command."""
|
|
162
|
+
if command not in AMP_COMMANDS:
|
|
163
|
+
raise ValueError(f"Unknown command: {command}")
|
|
164
|
+
|
|
165
|
+
func: Callable[..., Coroutine[Any, Any, Any]] = AMP_COMMANDS[command]
|
|
166
|
+
return await func(**kwargs)
|