@oswaldzsh/devhive 0.1.0
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.
- package/README.md +91 -0
- package/__init__.py +0 -0
- package/agents/__init__.py +0 -0
- package/agents/base.py +118 -0
- package/agents/execute.py +150 -0
- package/agents/verifier_dynamic.py +164 -0
- package/agents/verifier_semantic.py +84 -0
- package/agents/verifier_static.py +153 -0
- package/bin/dh +77 -0
- package/config.yaml +71 -0
- package/control_plane/__init__.py +0 -0
- package/control_plane/cli.py +596 -0
- package/control_plane/dashboard.py +57 -0
- package/control_plane/notifications.py +54 -0
- package/control_plane/tui.py +352 -0
- package/install.sh +67 -0
- package/orchestrator/__init__.py +0 -0
- package/orchestrator/agent_pool.py +107 -0
- package/orchestrator/convergence_gate.py +133 -0
- package/orchestrator/engine.py +353 -0
- package/orchestrator/event_bus.py +58 -0
- package/orchestrator/task_queue.py +59 -0
- package/package.json +50 -0
- package/protocol/__init__.py +0 -0
- package/protocol/schemas.py +222 -0
- package/setup.py +44 -0
- package/signature/__init__.py +0 -0
- package/signature/engine.py +211 -0
- package/signature/extractor.py +156 -0
- package/signature/learner.py +75 -0
- package/signature/src/matcher.c +263 -0
- package/signature/src/matcher.h +135 -0
- package/signatures/seed_signatures.json +174 -0
- package/storage/__init__.py +0 -0
- package/storage/checkpoint.py +153 -0
- package/storage/signature_db.py +62 -0
- package/tools/__init__.py +0 -0
- package/tools/api_client.py +101 -0
- package/tools/git.py +75 -0
- package/tools/sandbox.py +79 -0
- package/verification/__init__.py +0 -0
- package/verification/diagnostic.py +124 -0
- package/verification/patterns/api_breaking.yaml +25 -0
- package/verification/patterns/code_quality.yaml +41 -0
- package/verification/patterns/security.yaml +41 -0
- package/verification/pipeline.py +61 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Desktop notification helper for DevHive."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import platform
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def notify(title: str, message: str, urgency: str = "normal") -> bool:
|
|
9
|
+
"""Send a desktop notification. Returns True if successful."""
|
|
10
|
+
system = platform.system()
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
if system == "Linux":
|
|
14
|
+
subprocess.run(
|
|
15
|
+
["notify-send", "-u", urgency, title, message],
|
|
16
|
+
timeout=5, capture_output=True,
|
|
17
|
+
)
|
|
18
|
+
return True
|
|
19
|
+
elif system == "Darwin":
|
|
20
|
+
subprocess.run(
|
|
21
|
+
["osascript", "-e",
|
|
22
|
+
f'display notification "{message}" with title "{title}"'],
|
|
23
|
+
timeout=5, capture_output=True,
|
|
24
|
+
)
|
|
25
|
+
return True
|
|
26
|
+
elif system == "Windows":
|
|
27
|
+
# Windows toast notification via PowerShell
|
|
28
|
+
subprocess.run(
|
|
29
|
+
["powershell", "-Command",
|
|
30
|
+
f'New-BurntToastNotification -Text "{title}", "{message}"'],
|
|
31
|
+
timeout=5, capture_output=True,
|
|
32
|
+
)
|
|
33
|
+
return True
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def terminal_bell():
|
|
41
|
+
"""Ring the terminal bell."""
|
|
42
|
+
print("\a", end="", flush=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def escalate(task_id: str, reason: str):
|
|
46
|
+
"""Send escalation notification via all available channels."""
|
|
47
|
+
title = f"DevHive: {task_id}"
|
|
48
|
+
message = f"Needs attention: {reason}"
|
|
49
|
+
notify(title, message, urgency="critical")
|
|
50
|
+
terminal_bell()
|
|
51
|
+
print(f"\n{'!'*60}")
|
|
52
|
+
print(f" ESCALATION: {task_id}")
|
|
53
|
+
print(f" {reason}")
|
|
54
|
+
print(f"{'!'*60}\n")
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevHive TUI — Rich terminal UI components.
|
|
3
|
+
|
|
4
|
+
Layout:
|
|
5
|
+
┌─ Header ───────────────────────────────────────────────┐
|
|
6
|
+
│ Model: xxx | Uptime: 12m | Tasks: 3 done / 5 total │
|
|
7
|
+
├─ Pipeline ───────┬─ Detail ────────────────────────────┤
|
|
8
|
+
│ ● SPECIFY │ Task: fix-login-timeout │
|
|
9
|
+
│ ● EXECUTE │ Stage: VERIFY_L1 │
|
|
10
|
+
│ ◐ VERIFY_L1 │ Static ✓ Dynamic ◐ running │
|
|
11
|
+
│ ○ VERIFY_L2 │ Age: 45s │
|
|
12
|
+
│ ○ MERGE │ │
|
|
13
|
+
├─ Activity ───────┴─────────────────────────────────────┤
|
|
14
|
+
│ [14:30:01] execute-1 Started task-abc123 │
|
|
15
|
+
│ [14:30:15] static-v PASS - No issues found │
|
|
16
|
+
├─ Input ────────────────────────────────────────────────┤
|
|
17
|
+
│ :help :quit :do :log :review :status │
|
|
18
|
+
└────────────────────────────────────────────────────────┘
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import time
|
|
22
|
+
import asyncio
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from rich.console import Console, RenderableType
|
|
27
|
+
from rich.live import Live
|
|
28
|
+
from rich.layout import Layout
|
|
29
|
+
from rich.panel import Panel
|
|
30
|
+
from rich.table import Table
|
|
31
|
+
from rich.text import Text
|
|
32
|
+
from rich.align import Align
|
|
33
|
+
from rich.spinner import Spinner
|
|
34
|
+
from rich.columns import Columns
|
|
35
|
+
from rich import box
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
STAGE_ORDER = ["SPECIFY", "EXECUTE", "VERIFY_L1", "VERIFY_L2", "MERGE"]
|
|
39
|
+
STAGE_COLORS = {
|
|
40
|
+
"SPECIFY": "dim cyan",
|
|
41
|
+
"EXECUTE": "bright_yellow",
|
|
42
|
+
"VERIFY_L1": "bright_magenta",
|
|
43
|
+
"VERIFY_L2": "bright_blue",
|
|
44
|
+
"MERGE": "bright_green",
|
|
45
|
+
}
|
|
46
|
+
STAGE_SPINNERS = {
|
|
47
|
+
"SPECIFY": "dots",
|
|
48
|
+
"EXECUTE": "bouncingBar",
|
|
49
|
+
"VERIFY_L1": "arc",
|
|
50
|
+
"VERIFY_L2": "arc",
|
|
51
|
+
"MERGE": "dots",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DevHiveTUI:
|
|
56
|
+
"""Rich-based terminal UI for DevHive."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, orchestrator=None):
|
|
59
|
+
self.console = Console()
|
|
60
|
+
self._orchestrator = orchestrator
|
|
61
|
+
self._start_time = time.monotonic()
|
|
62
|
+
self._activity: list[tuple[str, str, str]] = [] # (time, source, message)
|
|
63
|
+
self._tasks: dict = {}
|
|
64
|
+
self._selected_task: Optional[str] = None
|
|
65
|
+
self._running = False
|
|
66
|
+
|
|
67
|
+
def log_activity(self, source: str, message: str):
|
|
68
|
+
"""Add an entry to the activity feed."""
|
|
69
|
+
ts = datetime.now(timezone.utc).replace(tzinfo=None).strftime("%H:%M:%S")
|
|
70
|
+
self._activity.append((ts, source, message))
|
|
71
|
+
if len(self._activity) > 100:
|
|
72
|
+
self._activity = self._activity[-50:]
|
|
73
|
+
|
|
74
|
+
def update_task(self, task_id: str, stage: str, status: str = "", detail: str = ""):
|
|
75
|
+
self._tasks[task_id] = {"stage": stage, "status": status, "detail": detail,
|
|
76
|
+
"updated": time.monotonic()}
|
|
77
|
+
|
|
78
|
+
def set_tasks(self, tasks: dict):
|
|
79
|
+
self._tasks = tasks
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def uptime(self) -> str:
|
|
83
|
+
seconds = int(time.monotonic() - self._start_time)
|
|
84
|
+
if seconds < 60:
|
|
85
|
+
return f"{seconds}s"
|
|
86
|
+
elif seconds < 3600:
|
|
87
|
+
return f"{seconds // 60}m {seconds % 60}s"
|
|
88
|
+
else:
|
|
89
|
+
h = seconds // 3600
|
|
90
|
+
m = (seconds % 3600) // 60
|
|
91
|
+
return f"{h}h {m}m"
|
|
92
|
+
|
|
93
|
+
def make_layout(self) -> Layout:
|
|
94
|
+
"""Build the main layout tree."""
|
|
95
|
+
layout = Layout()
|
|
96
|
+
layout.split(
|
|
97
|
+
Layout(name="header", size=3),
|
|
98
|
+
Layout(name="main"),
|
|
99
|
+
Layout(name="input", size=3),
|
|
100
|
+
)
|
|
101
|
+
layout["main"].split_row(
|
|
102
|
+
Layout(name="pipeline", ratio=1),
|
|
103
|
+
Layout(name="detail", ratio=2),
|
|
104
|
+
)
|
|
105
|
+
return layout
|
|
106
|
+
|
|
107
|
+
def _header(self) -> Panel:
|
|
108
|
+
model = "deepseek-v4-pro"
|
|
109
|
+
task_count = len(self._tasks)
|
|
110
|
+
active_count = sum(1 for t in self._tasks.values()
|
|
111
|
+
if t.get("stage") not in ("MERGE", ""))
|
|
112
|
+
escalations = sum(1 for t in self._tasks.values()
|
|
113
|
+
if t.get("status") == "ESCALATED")
|
|
114
|
+
|
|
115
|
+
grid = Table.grid(expand=True)
|
|
116
|
+
grid.add_column(justify="left")
|
|
117
|
+
grid.add_column(justify="right")
|
|
118
|
+
|
|
119
|
+
left = Text()
|
|
120
|
+
left.append("⚙ DevHive", style="bold white")
|
|
121
|
+
left.append(f" │ Model: {model}", style="dim")
|
|
122
|
+
left.append(f" │ Uptime: {self.uptime}", style="dim")
|
|
123
|
+
|
|
124
|
+
right = Text()
|
|
125
|
+
right.append(f"Tasks: {active_count} active", style="bright_cyan")
|
|
126
|
+
right.append(f" │ {task_count - active_count} done", style="dim green")
|
|
127
|
+
if escalations:
|
|
128
|
+
right.append(f" │ {escalations} escalated", style="bright_red")
|
|
129
|
+
|
|
130
|
+
grid.add_row(left, right)
|
|
131
|
+
return Panel(grid, style="bold", border_style="bright_black")
|
|
132
|
+
|
|
133
|
+
def _pipeline_panel(self) -> Panel:
|
|
134
|
+
"""Render the pipeline stage list with spinners."""
|
|
135
|
+
table = Table.grid(padding=(0, 1))
|
|
136
|
+
table.add_column(style="bold")
|
|
137
|
+
|
|
138
|
+
active_stages = set()
|
|
139
|
+
for t in self._tasks.values():
|
|
140
|
+
stage = t.get("stage", "")
|
|
141
|
+
if stage in STAGE_ORDER:
|
|
142
|
+
active_stages.add(stage)
|
|
143
|
+
|
|
144
|
+
for stage in STAGE_ORDER:
|
|
145
|
+
style = STAGE_COLORS.get(stage, "")
|
|
146
|
+
if stage in active_stages:
|
|
147
|
+
indicator = Spinner(STAGE_SPINNERS.get(stage, "dots"), text="",
|
|
148
|
+
style=style)
|
|
149
|
+
else:
|
|
150
|
+
indicator = Text("○", style="dim")
|
|
151
|
+
label = Text(f" {stage.replace('_', ' ')}", style=style if stage in active_stages else "dim")
|
|
152
|
+
table.add_row(Align.left(indicator), Align.left(label))
|
|
153
|
+
|
|
154
|
+
for _ in range(len(STAGE_ORDER), 7):
|
|
155
|
+
table.add_row(Text(""))
|
|
156
|
+
|
|
157
|
+
return Panel(table, title="Pipeline", border_style="bright_black",
|
|
158
|
+
title_align="left")
|
|
159
|
+
|
|
160
|
+
def _detail_panel(self) -> Panel:
|
|
161
|
+
"""Detail view for selected or most recent task."""
|
|
162
|
+
if self._selected_task and self._selected_task in self._tasks:
|
|
163
|
+
tasks_to_show = {self._selected_task: self._tasks[self._selected_task]}
|
|
164
|
+
elif self._tasks:
|
|
165
|
+
# Show most recently updated tasks
|
|
166
|
+
sorted_tasks = sorted(self._tasks.items(),
|
|
167
|
+
key=lambda x: x[1].get("updated", 0), reverse=True)
|
|
168
|
+
tasks_to_show = dict(sorted_tasks[:5])
|
|
169
|
+
else:
|
|
170
|
+
tasks_to_show = {}
|
|
171
|
+
|
|
172
|
+
if not tasks_to_show:
|
|
173
|
+
return Panel(
|
|
174
|
+
Align.center(Text("No tasks yet.\n\nType :do <description> to submit one.",
|
|
175
|
+
style="dim")),
|
|
176
|
+
title="Tasks", border_style="bright_black", title_align="left"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
content = Table.grid(padding=(0, 1))
|
|
180
|
+
content.add_column(style="bold", width=30)
|
|
181
|
+
content.add_column()
|
|
182
|
+
|
|
183
|
+
for task_id, info in tasks_to_show.items():
|
|
184
|
+
stage = info.get("stage", "?")
|
|
185
|
+
status = info.get("status", "")
|
|
186
|
+
detail = info.get("detail", "")
|
|
187
|
+
style = STAGE_COLORS.get(stage, "white")
|
|
188
|
+
|
|
189
|
+
# Task header
|
|
190
|
+
short_id = task_id[-16:] if len(task_id) > 16 else task_id
|
|
191
|
+
stage_icon = "◐" if stage not in ("MERGE", "") else "✓"
|
|
192
|
+
|
|
193
|
+
row_text = Text()
|
|
194
|
+
row_text.append(f"{stage_icon} ", style=style)
|
|
195
|
+
row_text.append(short_id, style="bold")
|
|
196
|
+
content.add_row(row_text, Text(detail[:80] or stage, style="dim"))
|
|
197
|
+
|
|
198
|
+
# Stage and status
|
|
199
|
+
stage_status = Text()
|
|
200
|
+
stage_status.append(stage, style=style)
|
|
201
|
+
if status:
|
|
202
|
+
stage_status.append(f" {status}", style="dim")
|
|
203
|
+
|
|
204
|
+
content.add_row(Text(""), stage_status)
|
|
205
|
+
content.add_row(Text(""), Text("")) # spacer
|
|
206
|
+
|
|
207
|
+
return Panel(content, title="Tasks", border_style="bright_black",
|
|
208
|
+
title_align="left")
|
|
209
|
+
|
|
210
|
+
def _activity_panel(self) -> Panel:
|
|
211
|
+
"""Activity feed showing recent events."""
|
|
212
|
+
recent = self._activity[-8:] if self._activity else []
|
|
213
|
+
if not recent:
|
|
214
|
+
return Panel(Text("Waiting for activity...", style="dim"),
|
|
215
|
+
title="Activity", border_style="bright_black",
|
|
216
|
+
title_align="left")
|
|
217
|
+
|
|
218
|
+
lines = []
|
|
219
|
+
for ts, source, msg in recent:
|
|
220
|
+
line = Text()
|
|
221
|
+
line.append(f"[{ts}] ", style="dim")
|
|
222
|
+
line.append(f"{source:<14}", style="bold cyan")
|
|
223
|
+
line.append(msg[:80], style="")
|
|
224
|
+
lines.append(line)
|
|
225
|
+
|
|
226
|
+
return Panel(Text("\n").join(lines),
|
|
227
|
+
title="Activity", border_style="bright_black",
|
|
228
|
+
title_align="left")
|
|
229
|
+
|
|
230
|
+
def _input_bar(self) -> Panel:
|
|
231
|
+
"""Command input bar."""
|
|
232
|
+
hints = Text(":help :quit :do :status :review :log :resolve",
|
|
233
|
+
style="dim")
|
|
234
|
+
return Panel(hints, border_style="bright_black")
|
|
235
|
+
|
|
236
|
+
def render(self) -> Layout:
|
|
237
|
+
layout = self.make_layout()
|
|
238
|
+
layout["header"].update(self._header())
|
|
239
|
+
layout["pipeline"].update(self._pipeline_panel())
|
|
240
|
+
layout["detail"].update(self._detail_panel())
|
|
241
|
+
layout["input"].update(self._input_bar())
|
|
242
|
+
return layout
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── Standalone display helpers (non-interactive) ─────────
|
|
246
|
+
|
|
247
|
+
def status_display(tasks: list[dict], escalations: list[dict]) -> RenderableType:
|
|
248
|
+
"""One-shot status display (for `dh status`)."""
|
|
249
|
+
console = Console()
|
|
250
|
+
|
|
251
|
+
# Header
|
|
252
|
+
console.print()
|
|
253
|
+
console.print(Panel(Text("DevHive Status", style="bold white"),
|
|
254
|
+
border_style="bright_blue"))
|
|
255
|
+
console.print(f" Active tasks: {len(tasks)}")
|
|
256
|
+
console.print(f" Open escalations: {len(escalations)}")
|
|
257
|
+
console.print()
|
|
258
|
+
|
|
259
|
+
# Tasks table
|
|
260
|
+
if tasks:
|
|
261
|
+
table = Table(title="Tasks", box=box.ROUNDED, border_style="bright_black")
|
|
262
|
+
table.add_column("Task ID", style="dim")
|
|
263
|
+
table.add_column("Stage")
|
|
264
|
+
table.add_column("Status")
|
|
265
|
+
|
|
266
|
+
for t in tasks:
|
|
267
|
+
stage = t.get("current_stage", "?")
|
|
268
|
+
style = STAGE_COLORS.get(stage, "white")
|
|
269
|
+
table.add_row(
|
|
270
|
+
t.get("id", "")[-24:],
|
|
271
|
+
f"[{style}]{stage}[/]",
|
|
272
|
+
t.get("status", "pending"),
|
|
273
|
+
)
|
|
274
|
+
console.print(table)
|
|
275
|
+
|
|
276
|
+
# Escalations
|
|
277
|
+
if escalations:
|
|
278
|
+
console.print()
|
|
279
|
+
for e in escalations:
|
|
280
|
+
console.print(Panel(
|
|
281
|
+
f"Task: {e.get('task_id', '?')}\n"
|
|
282
|
+
f"Escalation ID: {e.get('id', '?')}\n"
|
|
283
|
+
f"Created: {e.get('created_at', '?')}",
|
|
284
|
+
title="⚠ Escalation", border_style="red"
|
|
285
|
+
))
|
|
286
|
+
|
|
287
|
+
return ""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def task_timeline(checkpoints: list[dict]) -> RenderableType:
|
|
291
|
+
"""Render a task's execution timeline."""
|
|
292
|
+
console = Console()
|
|
293
|
+
|
|
294
|
+
if not checkpoints:
|
|
295
|
+
return Panel(Text("No history found.", style="dim"),
|
|
296
|
+
title="Task Timeline", border_style="yellow")
|
|
297
|
+
|
|
298
|
+
console.print()
|
|
299
|
+
console.print(Panel(Text("Task Timeline", style="bold white"),
|
|
300
|
+
border_style="bright_blue"))
|
|
301
|
+
console.print()
|
|
302
|
+
|
|
303
|
+
for i, cp in enumerate(checkpoints):
|
|
304
|
+
stage = cp.get("stage", "?")
|
|
305
|
+
outcome = cp.get("outcome", "?")
|
|
306
|
+
created = cp.get("created_at", "?")
|
|
307
|
+
style = STAGE_COLORS.get(stage, "white")
|
|
308
|
+
|
|
309
|
+
icon = "✓" if outcome == "COMPLETED" or outcome == "PASS" else \
|
|
310
|
+
"✗" if outcome == "FAIL" else \
|
|
311
|
+
"⚠" if outcome == "WARN" else "●"
|
|
312
|
+
|
|
313
|
+
color = "green" if outcome in ("COMPLETED", "PASS") else \
|
|
314
|
+
"red" if outcome == "FAIL" else \
|
|
315
|
+
"yellow" if outcome == "WARN" else "dim"
|
|
316
|
+
|
|
317
|
+
connector = "├─" if i < len(checkpoints) - 1 else "└─"
|
|
318
|
+
console.print(f" {connector} [{color}]{icon} {stage}[/] [{style}]{outcome}[/] [dim]{created}[/]")
|
|
319
|
+
|
|
320
|
+
console.print()
|
|
321
|
+
return ""
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def escalation_review(escalations: list[dict]) -> Optional[str]:
|
|
325
|
+
"""Interactive escalation review. Returns selected escalation_id or None."""
|
|
326
|
+
console = Console()
|
|
327
|
+
|
|
328
|
+
if not escalations:
|
|
329
|
+
console.print(Panel(Text("No open escalations! 🎉", style="green"),
|
|
330
|
+
border_style="green"))
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
console.print()
|
|
334
|
+
console.print(Panel(Text("Pending Escalations", style="bold white"),
|
|
335
|
+
border_style="bright_red"))
|
|
336
|
+
|
|
337
|
+
for i, e in enumerate(escalations):
|
|
338
|
+
console.print(f" [{i+1}] {e.get('id', '?')[:30]}")
|
|
339
|
+
console.print(f" Task: {e.get('task_id', '?')[-30:]}")
|
|
340
|
+
console.print(f" Created: {e.get('created_at', '?')}")
|
|
341
|
+
console.print()
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
choice = input(f" Select [1-{len(escalations)}] or Enter to skip: ").strip()
|
|
345
|
+
if choice.isdigit():
|
|
346
|
+
idx = int(choice) - 1
|
|
347
|
+
if 0 <= idx < len(escalations):
|
|
348
|
+
return escalations[idx].get("id")
|
|
349
|
+
except (EOFError, KeyboardInterrupt):
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
return None
|
package/install.sh
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# DevHive post-install script
|
|
3
|
+
# Installs Python dependencies and links the module.
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
DEVHIVE_HOME="${DEVHIVE_HOME:-$HOME/.devhive}"
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
|
|
10
|
+
echo ""
|
|
11
|
+
echo " ╔══════════════════════════════════╗"
|
|
12
|
+
echo " ║ DevHive v0.1.0 — Installing ║"
|
|
13
|
+
echo " ╚══════════════════════════════════╝"
|
|
14
|
+
echo ""
|
|
15
|
+
|
|
16
|
+
# 1. Find Python
|
|
17
|
+
PYTHON=""
|
|
18
|
+
for cmd in python3 python; do
|
|
19
|
+
if command -v "$cmd" &>/dev/null; then
|
|
20
|
+
PYTHON="$cmd"
|
|
21
|
+
break
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
if [ -z "$PYTHON" ]; then
|
|
26
|
+
echo " ⚠ Python 3 not found. Please install Python 3.12+ first."
|
|
27
|
+
echo " ⚠ DevHive installed but Python components are not available."
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo " Python: $PYTHON ($($PYTHON --version))"
|
|
32
|
+
|
|
33
|
+
# 2. Install pip deps
|
|
34
|
+
echo " Installing Python dependencies..."
|
|
35
|
+
$PYTHON -m pip install --quiet httpx pydantic pyyaml rich 2>/dev/null || {
|
|
36
|
+
echo " ⚠ pip install failed. Please run manually:"
|
|
37
|
+
echo " pip3 install httpx pydantic pyyaml rich"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# 3. Link module
|
|
41
|
+
mkdir -p "$DEVHIVE_HOME"
|
|
42
|
+
if [ "$SCRIPT_DIR" != "$DEVHIVE_HOME" ]; then
|
|
43
|
+
echo " Linking module to $DEVHIVE_HOME..."
|
|
44
|
+
# Copy module files
|
|
45
|
+
for dir in control_plane agents orchestrator protocol verification signature storage tools; do
|
|
46
|
+
if [ -d "$SCRIPT_DIR/$dir" ]; then
|
|
47
|
+
cp -r "$SCRIPT_DIR/$dir" "$DEVHIVE_HOME/"
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
# Copy root files
|
|
51
|
+
for f in __init__.py config.yaml setup.py; do
|
|
52
|
+
if [ -f "$SCRIPT_DIR/$f" ]; then
|
|
53
|
+
cp "$SCRIPT_DIR/$f" "$DEVHIVE_HOME/"
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# 4. Verify installation
|
|
59
|
+
echo ""
|
|
60
|
+
echo " ✓ DevHive installed successfully!"
|
|
61
|
+
echo ""
|
|
62
|
+
echo " Quick start:"
|
|
63
|
+
echo " dh # Interactive REPL with live dashboard"
|
|
64
|
+
echo " dh do \"fix bug\" # Submit a task"
|
|
65
|
+
echo " dh status # Check system status"
|
|
66
|
+
echo " dh review # Review escalations"
|
|
67
|
+
echo ""
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Agent Pool Manager — manages agent process lifecycle."""
|
|
2
|
+
|
|
3
|
+
import multiprocessing
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from protocol.schemas import Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentType(str, Enum):
|
|
12
|
+
EXECUTE = "execute"
|
|
13
|
+
STATIC_VERIFIER = "static_verifier"
|
|
14
|
+
DYNAMIC_VERIFIER = "dynamic_verifier"
|
|
15
|
+
SEMANTIC_VERIFIER = "semantic_verifier"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AgentHandle:
|
|
20
|
+
agent_id: str
|
|
21
|
+
agent_type: AgentType
|
|
22
|
+
process: multiprocessing.Process
|
|
23
|
+
task_queue: multiprocessing.Queue
|
|
24
|
+
result_queue: multiprocessing.Queue
|
|
25
|
+
busy: bool = False
|
|
26
|
+
current_task_id: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AgentPool:
|
|
30
|
+
"""Manages a pool of agent processes with task dispatching."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: dict = None):
|
|
33
|
+
self.config = config or {}
|
|
34
|
+
self.agents: dict[str, AgentHandle] = {}
|
|
35
|
+
self._next_id: dict[AgentType, int] = {}
|
|
36
|
+
|
|
37
|
+
def start_agent(self, agent_type: AgentType) -> AgentHandle:
|
|
38
|
+
"""Start a new agent process."""
|
|
39
|
+
from agents.execute import ExecuteAgent
|
|
40
|
+
from agents.verifier_static import StaticVerifier
|
|
41
|
+
from agents.verifier_dynamic import DynamicVerifier
|
|
42
|
+
from agents.verifier_semantic import SemanticVerifier
|
|
43
|
+
|
|
44
|
+
idx = self._next_id.get(agent_type, 0) + 1
|
|
45
|
+
self._next_id[agent_type] = idx
|
|
46
|
+
agent_id = f"{agent_type.value}-{idx}"
|
|
47
|
+
|
|
48
|
+
task_queue = multiprocessing.Queue()
|
|
49
|
+
result_queue = multiprocessing.Queue()
|
|
50
|
+
|
|
51
|
+
agent_classes = {
|
|
52
|
+
AgentType.EXECUTE: ExecuteAgent,
|
|
53
|
+
AgentType.STATIC_VERIFIER: StaticVerifier,
|
|
54
|
+
AgentType.DYNAMIC_VERIFIER: DynamicVerifier,
|
|
55
|
+
AgentType.SEMANTIC_VERIFIER: SemanticVerifier,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
agent_cls = agent_classes[agent_type]
|
|
59
|
+
agent = agent_cls(agent_id, task_queue, result_queue, self.config)
|
|
60
|
+
agent.start()
|
|
61
|
+
|
|
62
|
+
handle = AgentHandle(
|
|
63
|
+
agent_id=agent_id,
|
|
64
|
+
agent_type=agent_type,
|
|
65
|
+
process=agent,
|
|
66
|
+
task_queue=task_queue,
|
|
67
|
+
result_queue=result_queue,
|
|
68
|
+
)
|
|
69
|
+
self.agents[agent_id] = handle
|
|
70
|
+
return handle
|
|
71
|
+
|
|
72
|
+
def get_idle(self, agent_type: AgentType) -> Optional[AgentHandle]:
|
|
73
|
+
"""Find an idle agent of the specified type."""
|
|
74
|
+
for handle in self.agents.values():
|
|
75
|
+
if handle.agent_type == agent_type and not handle.busy:
|
|
76
|
+
return handle
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def dispatch(self, handle: AgentHandle, task: Task):
|
|
80
|
+
"""Send a task to an idle agent."""
|
|
81
|
+
handle.busy = True
|
|
82
|
+
handle.current_task_id = task.id
|
|
83
|
+
handle.task_queue.put(task.model_dump())
|
|
84
|
+
|
|
85
|
+
def mark_idle(self, agent_id: str):
|
|
86
|
+
"""Mark an agent as idle after task completion."""
|
|
87
|
+
if agent_id in self.agents:
|
|
88
|
+
self.agents[agent_id].busy = False
|
|
89
|
+
self.agents[agent_id].current_task_id = None
|
|
90
|
+
|
|
91
|
+
def stop_all(self):
|
|
92
|
+
"""Stop all agent processes."""
|
|
93
|
+
for handle in self.agents.values():
|
|
94
|
+
handle.task_queue.put(None) # poison pill
|
|
95
|
+
handle.process.join(timeout=10)
|
|
96
|
+
if handle.process.is_alive():
|
|
97
|
+
handle.process.terminate()
|
|
98
|
+
|
|
99
|
+
def idle_count(self, agent_type: AgentType) -> int:
|
|
100
|
+
"""Count idle agents of a given type."""
|
|
101
|
+
return sum(1 for h in self.agents.values()
|
|
102
|
+
if h.agent_type == agent_type and not h.busy)
|
|
103
|
+
|
|
104
|
+
def total_count(self, agent_type: AgentType) -> int:
|
|
105
|
+
"""Count total agents of a given type."""
|
|
106
|
+
return sum(1 for h in self.agents.values()
|
|
107
|
+
if h.agent_type == agent_type)
|