@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,596 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
dh — DevHive CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
dh Start interactive REPL with live dashboard
|
|
7
|
+
dh do <description> Submit a task, auto-parse into TaskSpec
|
|
8
|
+
dh do -f <file> Submit a task from JSON/YAML spec file
|
|
9
|
+
dh run Start orchestrator daemon
|
|
10
|
+
dh status Show system status
|
|
11
|
+
dh log <task-id> Show task execution timeline
|
|
12
|
+
dh review Interactive escalation review
|
|
13
|
+
dh resolve <id> Resolve an escalation
|
|
14
|
+
dh !<shell command> Escape to shell (from interactive mode)
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
dh do "fix the login timeout bug in auth/session.py"
|
|
18
|
+
dh do "add rate limiting middleware to all API endpoints" -p HIGH
|
|
19
|
+
dh do -f task_spec.yaml
|
|
20
|
+
dh status
|
|
21
|
+
dh log task-20260624-143000-abc123
|
|
22
|
+
dh review
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import asyncio
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import shlex
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Optional
|
|
35
|
+
|
|
36
|
+
# Rich imports
|
|
37
|
+
from rich.console import Console
|
|
38
|
+
from rich.live import Live
|
|
39
|
+
from rich.panel import Panel
|
|
40
|
+
from rich.text import Text
|
|
41
|
+
from rich.table import Table
|
|
42
|
+
from rich import box
|
|
43
|
+
|
|
44
|
+
# DevHive imports
|
|
45
|
+
from protocol.schemas import TaskSpec, Priority
|
|
46
|
+
from orchestrator.engine import Orchestrator
|
|
47
|
+
from control_plane.tui import (
|
|
48
|
+
DevHiveTUI, status_display, task_timeline, escalation_review,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
VERSION = "0.1.0"
|
|
53
|
+
BANNER = r"""
|
|
54
|
+
╔══════════════════════════════════════════╗
|
|
55
|
+
║ ____ __ __ _ ║
|
|
56
|
+
║ / __ \ ___ __ __/ / / / (_) _____ ║
|
|
57
|
+
║ / / / // _ \\ \ / / /_/ / / / | / / _ \ ║
|
|
58
|
+
║/ /_/ /| __/\ V / __ / / /| |/ / __/ ║
|
|
59
|
+
║\_____/ \___| \_/ /_/ /_/ /_/ |___/\___| ║
|
|
60
|
+
║ ║
|
|
61
|
+
║ Multi-Agent Software Development ║
|
|
62
|
+
╚══════════════════════════════════════════╝
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
HELP_TEXT = """
|
|
66
|
+
Commands:
|
|
67
|
+
:do, :d <desc> Submit a task from description
|
|
68
|
+
:status, :st Show system status
|
|
69
|
+
:log, :l <id> Show task execution timeline
|
|
70
|
+
:review, :rv Review pending escalations
|
|
71
|
+
:resolve, :rs <id> Resolve an escalation
|
|
72
|
+
:tasks, :t List all tasks
|
|
73
|
+
:watch <id> Watch a specific task in detail
|
|
74
|
+
:quit, :q Exit DevHive
|
|
75
|
+
|
|
76
|
+
Shortcuts:
|
|
77
|
+
!<cmd> Escape to shell (e.g., !git status)
|
|
78
|
+
Ctrl+C Interrupt current operation
|
|
79
|
+
|
|
80
|
+
From terminal:
|
|
81
|
+
dh do "description" Quick task submission
|
|
82
|
+
dh status Quick status check
|
|
83
|
+
dh review Quick escalation review
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── Config ──────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def load_config(config_path: str = None) -> dict:
|
|
90
|
+
"""Load YAML config, resolving env vars."""
|
|
91
|
+
import yaml
|
|
92
|
+
paths = [
|
|
93
|
+
config_path,
|
|
94
|
+
os.path.join(os.path.dirname(__file__), "..", "config.yaml"),
|
|
95
|
+
os.path.join(os.path.dirname(__file__), "..", "config.local.yaml"),
|
|
96
|
+
os.path.expanduser("~/.config/devhive/config.yaml"),
|
|
97
|
+
]
|
|
98
|
+
for p in paths:
|
|
99
|
+
if p and os.path.exists(p):
|
|
100
|
+
with open(p) as f:
|
|
101
|
+
raw = f.read()
|
|
102
|
+
# Resolve ${VAR} placeholders
|
|
103
|
+
import re
|
|
104
|
+
def _sub(m):
|
|
105
|
+
return os.environ.get(m.group(1), m.group(0))
|
|
106
|
+
raw = re.sub(r'\$\{(\w+)\}', _sub, raw)
|
|
107
|
+
return yaml.safe_load(raw)
|
|
108
|
+
return {}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── Natural Language Task Parsing ────────────────────────────
|
|
112
|
+
|
|
113
|
+
async def parse_task_from_nl(description: str, priority: str = "MEDIUM",
|
|
114
|
+
config: dict = None) -> TaskSpec:
|
|
115
|
+
"""Use the model to parse a natural language description into a TaskSpec."""
|
|
116
|
+
from tools.api_client import APIClient, extract_text_from_response
|
|
117
|
+
|
|
118
|
+
config = config or {}
|
|
119
|
+
api_cfg = config.get("api", {})
|
|
120
|
+
client = APIClient(
|
|
121
|
+
base_url=api_cfg.get("base_url"),
|
|
122
|
+
auth_token=api_cfg.get("auth_token"),
|
|
123
|
+
default_model=api_cfg.get("default_model"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
system = """You are a task parser. Convert the user's natural language description
|
|
127
|
+
into a structured JSON task specification. Output ONLY the JSON, no explanation.
|
|
128
|
+
|
|
129
|
+
Format:
|
|
130
|
+
{
|
|
131
|
+
"title": "concise one-line title",
|
|
132
|
+
"description": "expanded description with context and requirements",
|
|
133
|
+
"acceptance_criteria": ["criterion 1", "criterion 2"],
|
|
134
|
+
"scope_constraints": ["constraint 1"],
|
|
135
|
+
"sensitive_modules": [],
|
|
136
|
+
"priority": "MEDIUM"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
Rules:
|
|
140
|
+
- title: one line, action-oriented (start with a verb)
|
|
141
|
+
- description: include what, why, and any context the user provided
|
|
142
|
+
- acceptance_criteria: specific, testable conditions of done
|
|
143
|
+
- scope_constraints: what NOT to touch
|
|
144
|
+
- sensitive_modules: detect if auth/payment/data/permission is involved
|
|
145
|
+
- priority: use the provided priority if specified
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
response = await client.create_message(
|
|
149
|
+
system=system,
|
|
150
|
+
messages=[{"role": "user", "content": f"Priority: {priority}\n\n{description}"}],
|
|
151
|
+
max_tokens=1024,
|
|
152
|
+
temperature=0.1,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
text = extract_text_from_response(response)
|
|
156
|
+
|
|
157
|
+
# Extract JSON
|
|
158
|
+
import re
|
|
159
|
+
match = re.search(r'\{.*\}', text, re.DOTALL)
|
|
160
|
+
if match:
|
|
161
|
+
data = json.loads(match.group(0))
|
|
162
|
+
else:
|
|
163
|
+
# Fallback: build simple spec
|
|
164
|
+
data = {
|
|
165
|
+
"title": description[:80],
|
|
166
|
+
"description": description,
|
|
167
|
+
"acceptance_criteria": [],
|
|
168
|
+
"scope_constraints": [],
|
|
169
|
+
"sensitive_modules": [],
|
|
170
|
+
"priority": priority,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return TaskSpec(
|
|
174
|
+
title=data.get("title", description[:80]),
|
|
175
|
+
description=data.get("description", description),
|
|
176
|
+
acceptance_criteria=data.get("acceptance_criteria", []),
|
|
177
|
+
scope_constraints=data.get("scope_constraints", []),
|
|
178
|
+
sensitive_modules=data.get("sensitive_modules", []),
|
|
179
|
+
priority=Priority(data.get("priority", priority)),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── Interactive REPL ────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
class DevHiveREPL:
|
|
186
|
+
"""Interactive REPL with live dashboard."""
|
|
187
|
+
|
|
188
|
+
def __init__(self, config: dict):
|
|
189
|
+
self.config = config
|
|
190
|
+
self.console = Console()
|
|
191
|
+
self.orchestrator: Optional[Orchestrator] = None
|
|
192
|
+
self.tui = DevHiveTUI()
|
|
193
|
+
self._running = False
|
|
194
|
+
self._command_task: Optional[asyncio.Task] = None
|
|
195
|
+
|
|
196
|
+
async def start(self):
|
|
197
|
+
"""Start orchestrator and enter interactive loop."""
|
|
198
|
+
self.console.print(BANNER, style="bold cyan")
|
|
199
|
+
self.console.print(f" Version {VERSION}", style="dim")
|
|
200
|
+
model = self.config.get("api", {}).get("default_model", "unknown")
|
|
201
|
+
self.console.print(f" Model: {model}", style="dim")
|
|
202
|
+
self.console.print()
|
|
203
|
+
|
|
204
|
+
# Start orchestrator
|
|
205
|
+
self.orchestrator = Orchestrator(self.config)
|
|
206
|
+
self.tui._orchestrator = self.orchestrator
|
|
207
|
+
|
|
208
|
+
# Start orchestrator in background
|
|
209
|
+
orchestrator_task = asyncio.create_task(
|
|
210
|
+
self.orchestrator.start(agent_counts={"execute": 1})
|
|
211
|
+
)
|
|
212
|
+
await asyncio.sleep(0.5) # Let it initialize
|
|
213
|
+
|
|
214
|
+
self.tui.log_activity("system", "Orchestrator started")
|
|
215
|
+
self._running = True
|
|
216
|
+
|
|
217
|
+
# Start TUI refresh and command input concurrently
|
|
218
|
+
try:
|
|
219
|
+
await self._repl_loop()
|
|
220
|
+
except (KeyboardInterrupt, EOFError):
|
|
221
|
+
pass
|
|
222
|
+
finally:
|
|
223
|
+
self._running = False
|
|
224
|
+
await self.orchestrator.stop()
|
|
225
|
+
orchestrator_task.cancel()
|
|
226
|
+
try:
|
|
227
|
+
await orchestrator_task
|
|
228
|
+
except asyncio.CancelledError:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
self.console.print("\n DevHive stopped.", style="dim")
|
|
232
|
+
|
|
233
|
+
async def _repl_loop(self):
|
|
234
|
+
"""Main REPL: live dashboard + command input."""
|
|
235
|
+
from rich.live import Live
|
|
236
|
+
from prompt_toolkit import PromptSession
|
|
237
|
+
from prompt_toolkit.history import FileHistory
|
|
238
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
239
|
+
|
|
240
|
+
# Try prompt_toolkit for fancy input; fall back to input()
|
|
241
|
+
try:
|
|
242
|
+
session = PromptSession(
|
|
243
|
+
history=FileHistory(os.path.expanduser("~/.devhive_history")),
|
|
244
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
245
|
+
)
|
|
246
|
+
use_prompt_toolkit = True
|
|
247
|
+
except ImportError:
|
|
248
|
+
session = None
|
|
249
|
+
use_prompt_toolkit = False
|
|
250
|
+
|
|
251
|
+
with Live(self.tui.render(), console=self.console,
|
|
252
|
+
refresh_per_second=4, screen=True) as live:
|
|
253
|
+
|
|
254
|
+
# Background task to push TUI updates
|
|
255
|
+
async def _refresh_tui():
|
|
256
|
+
while self._running:
|
|
257
|
+
live.update(self.tui.render())
|
|
258
|
+
await asyncio.sleep(0.25)
|
|
259
|
+
|
|
260
|
+
refresh_task = asyncio.create_task(_refresh_tui())
|
|
261
|
+
|
|
262
|
+
# Command input loop
|
|
263
|
+
while self._running:
|
|
264
|
+
try:
|
|
265
|
+
live.stop() # Pause TUI for input
|
|
266
|
+
if use_prompt_toolkit:
|
|
267
|
+
cmd = await asyncio.get_event_loop().run_in_executor(
|
|
268
|
+
None, lambda: session.prompt("dh> "))
|
|
269
|
+
else:
|
|
270
|
+
cmd = await asyncio.get_event_loop().run_in_executor(
|
|
271
|
+
None, lambda: input("dh> "))
|
|
272
|
+
live.start()
|
|
273
|
+
|
|
274
|
+
if cmd:
|
|
275
|
+
await self._handle_command(cmd.strip())
|
|
276
|
+
except (KeyboardInterrupt, EOFError):
|
|
277
|
+
break
|
|
278
|
+
except Exception as e:
|
|
279
|
+
self.tui.log_activity("error", str(e))
|
|
280
|
+
|
|
281
|
+
refresh_task.cancel()
|
|
282
|
+
try:
|
|
283
|
+
await refresh_task
|
|
284
|
+
except asyncio.CancelledError:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
async def _handle_command(self, cmd: str):
|
|
288
|
+
"""Dispatch a REPL command."""
|
|
289
|
+
# Shell escape
|
|
290
|
+
if cmd.startswith("!"):
|
|
291
|
+
shell_cmd = cmd[1:].strip()
|
|
292
|
+
if shell_cmd:
|
|
293
|
+
self.tui.log_activity("shell", shell_cmd)
|
|
294
|
+
try:
|
|
295
|
+
result = subprocess.run(
|
|
296
|
+
shell_cmd, shell=True, capture_output=True,
|
|
297
|
+
text=True, timeout=30
|
|
298
|
+
)
|
|
299
|
+
output = result.stdout or result.stderr
|
|
300
|
+
for line in output.strip().split("\n")[:20]:
|
|
301
|
+
self.tui.log_activity("shell", line)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
self.tui.log_activity("shell", f"Error: {e}")
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# Colon commands
|
|
307
|
+
if cmd.startswith(":"):
|
|
308
|
+
parts = shlex.split(cmd)
|
|
309
|
+
cmd_name = parts[0][1:].lower() if parts else ""
|
|
310
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
311
|
+
else:
|
|
312
|
+
# Bare text → submit as task
|
|
313
|
+
cmd_name = "do"
|
|
314
|
+
args = [cmd]
|
|
315
|
+
|
|
316
|
+
match cmd_name:
|
|
317
|
+
case "help" | "h":
|
|
318
|
+
self.console.print(HELP_TEXT, style="dim")
|
|
319
|
+
self.tui.log_activity("system", "Showed help")
|
|
320
|
+
|
|
321
|
+
case "quit" | "q" | "exit":
|
|
322
|
+
self._running = False
|
|
323
|
+
self.tui.log_activity("system", "Shutting down...")
|
|
324
|
+
|
|
325
|
+
case "do" | "d":
|
|
326
|
+
await self._cmd_submit(" ".join(args) if args else "")
|
|
327
|
+
|
|
328
|
+
case "status" | "st":
|
|
329
|
+
await self._cmd_show_status()
|
|
330
|
+
|
|
331
|
+
case "tasks" | "t":
|
|
332
|
+
await self._cmd_list_tasks()
|
|
333
|
+
|
|
334
|
+
case "log" | "l":
|
|
335
|
+
await self._cmd_show_log(args[0] if args else None)
|
|
336
|
+
|
|
337
|
+
case "review" | "rv":
|
|
338
|
+
await self._cmd_review()
|
|
339
|
+
|
|
340
|
+
case "resolve" | "rs":
|
|
341
|
+
if args:
|
|
342
|
+
await self._cmd_resolve(args[0])
|
|
343
|
+
|
|
344
|
+
case "watch" | "w":
|
|
345
|
+
if args:
|
|
346
|
+
self.tui._selected_task = args[0]
|
|
347
|
+
self.tui.log_activity("system", f"Watching task {args[0]}")
|
|
348
|
+
|
|
349
|
+
case _:
|
|
350
|
+
# Unknown command → treat as task description
|
|
351
|
+
desc = cmd[1:] if cmd.startswith(":") else cmd
|
|
352
|
+
await self._cmd_submit(desc)
|
|
353
|
+
|
|
354
|
+
async def _cmd_submit(self, description: str):
|
|
355
|
+
"""Submit a new task."""
|
|
356
|
+
if not description:
|
|
357
|
+
self.tui.log_activity("error", "Usage: :do <description>")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
self.tui.log_activity("system", f"Parsing: {description[:60]}...")
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
spec = await parse_task_from_nl(description, config=self.config)
|
|
364
|
+
task_id = await self.orchestrator.submit_task(spec)
|
|
365
|
+
self.tui.update_task(task_id, "SPECIFY", "parsed",
|
|
366
|
+
spec.title)
|
|
367
|
+
self.tui.log_activity("task", f"Created {task_id[-16:]}: {spec.title}")
|
|
368
|
+
self.console.print(f" ✓ Task created: {task_id}", style="green")
|
|
369
|
+
except Exception as e:
|
|
370
|
+
self.tui.log_activity("error", f"Failed: {e}")
|
|
371
|
+
|
|
372
|
+
async def _cmd_show_status(self):
|
|
373
|
+
"""Show system status."""
|
|
374
|
+
store = self.orchestrator.checkpoint
|
|
375
|
+
tasks = store.get_pending_tasks()
|
|
376
|
+
escs = store.get_open_escalations()
|
|
377
|
+
status_display(tasks, escs)
|
|
378
|
+
|
|
379
|
+
async def _cmd_list_tasks(self):
|
|
380
|
+
"""List all tasks in the TUI detail panel."""
|
|
381
|
+
store = self.orchestrator.checkpoint
|
|
382
|
+
tasks = store.get_pending_tasks()
|
|
383
|
+
for t in tasks:
|
|
384
|
+
stage = t.get("current_stage", "?")
|
|
385
|
+
self.tui.update_task(t["id"], stage, t.get("status", ""),
|
|
386
|
+
t.get("spec_json", "{}")[:80])
|
|
387
|
+
self.tui.log_activity("system", f"Loaded {len(tasks)} tasks")
|
|
388
|
+
|
|
389
|
+
async def _cmd_show_log(self, task_id: str = None):
|
|
390
|
+
"""Show task execution timeline."""
|
|
391
|
+
if not task_id:
|
|
392
|
+
task_id = self.tui._selected_task
|
|
393
|
+
if not task_id:
|
|
394
|
+
self.tui.log_activity("error", "Usage: :log <task-id>")
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
store = self.orchestrator.checkpoint
|
|
398
|
+
history = store.get_task_history(task_id)
|
|
399
|
+
task_timeline(history)
|
|
400
|
+
|
|
401
|
+
async def _cmd_review(self):
|
|
402
|
+
"""Interactive escalation review."""
|
|
403
|
+
store = self.orchestrator.checkpoint
|
|
404
|
+
escs = store.get_open_escalations()
|
|
405
|
+
selected = escalation_review(escs)
|
|
406
|
+
if selected:
|
|
407
|
+
self.console.print(f"\n Resolving {selected}...")
|
|
408
|
+
store.resolve_escalation(selected, "human-operator")
|
|
409
|
+
self.tui.log_activity("system", f"Resolved escalation {selected[:20]}...")
|
|
410
|
+
self.console.print(" ✓ Resolved", style="green")
|
|
411
|
+
|
|
412
|
+
async def _cmd_resolve(self, esc_id: str):
|
|
413
|
+
"""Resolve an escalation by ID."""
|
|
414
|
+
self.orchestrator.checkpoint.resolve_escalation(esc_id, "human-operator")
|
|
415
|
+
self.tui.log_activity("system", f"Resolved {esc_id[:20]}...")
|
|
416
|
+
self.console.print(f" ✓ Escalation resolved", style="green")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ── One-shot Commands ───────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
async def cmd_do(args, config: dict):
|
|
422
|
+
"""Quick task submission from the command line."""
|
|
423
|
+
console = Console()
|
|
424
|
+
console.print(BANNER, style="bold cyan")
|
|
425
|
+
|
|
426
|
+
if args.file:
|
|
427
|
+
import yaml
|
|
428
|
+
with open(args.file) as f:
|
|
429
|
+
data = json.load(f) if args.file.endswith(".json") else yaml.safe_load(f)
|
|
430
|
+
spec = TaskSpec(**data)
|
|
431
|
+
console.print(f" Loaded spec from {args.file}", style="dim")
|
|
432
|
+
elif args.description:
|
|
433
|
+
console.print(" Parsing task description...", style="dim")
|
|
434
|
+
spec = await parse_task_from_nl(
|
|
435
|
+
args.description,
|
|
436
|
+
priority=args.priority or "MEDIUM",
|
|
437
|
+
config=config,
|
|
438
|
+
)
|
|
439
|
+
console.print(f" Title: {spec.title}", style="bold")
|
|
440
|
+
console.print(f" Priority: {spec.priority.value}", style="dim")
|
|
441
|
+
else:
|
|
442
|
+
console.print(" Usage: dh do <description> or dh do -f <file>", style="red")
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
orch = Orchestrator(config)
|
|
446
|
+
await orch.start(agent_counts={"execute": 1})
|
|
447
|
+
await asyncio.sleep(0.3)
|
|
448
|
+
|
|
449
|
+
task_id = await orch.submit_task(spec)
|
|
450
|
+
console.print(f"\n ✓ Task submitted: {task_id}", style="green")
|
|
451
|
+
|
|
452
|
+
# Wait briefly for execution to start
|
|
453
|
+
await asyncio.sleep(2)
|
|
454
|
+
|
|
455
|
+
# Show task status
|
|
456
|
+
history = orch.checkpoint.get_task_history(task_id)
|
|
457
|
+
if history:
|
|
458
|
+
task_timeline(history)
|
|
459
|
+
|
|
460
|
+
await orch.stop()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
async def cmd_status(args, config: dict):
|
|
464
|
+
"""Quick status check."""
|
|
465
|
+
orch = Orchestrator(config)
|
|
466
|
+
await orch.start(agent_counts={})
|
|
467
|
+
await asyncio.sleep(0.3)
|
|
468
|
+
|
|
469
|
+
tasks = orch.checkpoint.get_pending_tasks()
|
|
470
|
+
escs = orch.checkpoint.get_open_escalations()
|
|
471
|
+
status_display(tasks, escs)
|
|
472
|
+
|
|
473
|
+
await orch.stop()
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
async def cmd_log(args, config: dict):
|
|
477
|
+
"""Show task timeline."""
|
|
478
|
+
orch = Orchestrator(config)
|
|
479
|
+
await orch.start(agent_counts={})
|
|
480
|
+
|
|
481
|
+
history = orch.checkpoint.get_task_history(args.task_id)
|
|
482
|
+
task_timeline(history)
|
|
483
|
+
|
|
484
|
+
await orch.stop()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
async def cmd_review(args, config: dict):
|
|
488
|
+
"""Interactive escalation review."""
|
|
489
|
+
orch = Orchestrator(config)
|
|
490
|
+
await orch.start(agent_counts={})
|
|
491
|
+
|
|
492
|
+
escs = orch.checkpoint.get_open_escalations()
|
|
493
|
+
selected = escalation_review(escs)
|
|
494
|
+
|
|
495
|
+
if selected:
|
|
496
|
+
orch.checkpoint.resolve_escalation(selected, "human-operator")
|
|
497
|
+
Console().print(f"\n ✓ Resolved", style="green")
|
|
498
|
+
|
|
499
|
+
await orch.stop()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
async def cmd_resolve(args, config: dict):
|
|
503
|
+
"""Resolve an escalation."""
|
|
504
|
+
orch = Orchestrator(config)
|
|
505
|
+
await orch.start(agent_counts={})
|
|
506
|
+
orch.checkpoint.resolve_escalation(args.escalation_id, "human-operator")
|
|
507
|
+
Console().print(f" ✓ Resolved {args.escalation_id}", style="green")
|
|
508
|
+
await orch.stop()
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# ── Daemon ──────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
async def cmd_run(args, config: dict):
|
|
514
|
+
"""Start orchestrator daemon (for long-running use)."""
|
|
515
|
+
console = Console()
|
|
516
|
+
console.print(BANNER, style="bold cyan")
|
|
517
|
+
console.print(" Starting DevHive daemon...", style="dim")
|
|
518
|
+
|
|
519
|
+
orch = Orchestrator(config)
|
|
520
|
+
await orch.start(agent_counts=args.agents)
|
|
521
|
+
console.print(" ✓ Daemon running. Press Ctrl+C to stop.", style="green")
|
|
522
|
+
console.print()
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
await asyncio.Event().wait()
|
|
526
|
+
except KeyboardInterrupt:
|
|
527
|
+
console.print("\n Shutting down...", style="dim")
|
|
528
|
+
await orch.stop()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ── Entry Point ─────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
def main():
|
|
534
|
+
parser = argparse.ArgumentParser(
|
|
535
|
+
prog="dh",
|
|
536
|
+
description="DevHive — Multi-Agent Software Development System",
|
|
537
|
+
)
|
|
538
|
+
parser.add_argument("-c", "--config", help="Config file path")
|
|
539
|
+
parser.add_argument("-v", "--version", action="version",
|
|
540
|
+
version=f"DevHive {VERSION}")
|
|
541
|
+
sub = parser.add_subparsers(dest="command")
|
|
542
|
+
|
|
543
|
+
# dh (no args) → interactive REPL
|
|
544
|
+
# (handled by checking if command is None below)
|
|
545
|
+
|
|
546
|
+
# dh do
|
|
547
|
+
p_do = sub.add_parser("do", help="Submit a task")
|
|
548
|
+
p_do.add_argument("description", nargs="?", help="Task description in natural language")
|
|
549
|
+
p_do.add_argument("-f", "--file", help="Task spec JSON/YAML file")
|
|
550
|
+
p_do.add_argument("-p", "--priority", choices=["CRITICAL", "HIGH", "MEDIUM", "LOW"])
|
|
551
|
+
|
|
552
|
+
# dh run
|
|
553
|
+
p_run = sub.add_parser("run", help="Start orchestrator daemon")
|
|
554
|
+
p_run.add_argument("-a", "--agents", type=int, default=1,
|
|
555
|
+
help="Number of execute agents")
|
|
556
|
+
|
|
557
|
+
# dh status
|
|
558
|
+
sub.add_parser("status", help="Show system status")
|
|
559
|
+
|
|
560
|
+
# dh log
|
|
561
|
+
p_log = sub.add_parser("log", help="Show task execution timeline")
|
|
562
|
+
p_log.add_argument("task_id", help="Task ID")
|
|
563
|
+
|
|
564
|
+
# dh review
|
|
565
|
+
sub.add_parser("review", help="Interactive escalation review")
|
|
566
|
+
|
|
567
|
+
# dh resolve
|
|
568
|
+
p_resolve = sub.add_parser("resolve", help="Resolve an escalation")
|
|
569
|
+
p_resolve.add_argument("escalation_id", help="Escalation ID")
|
|
570
|
+
|
|
571
|
+
args = parser.parse_args()
|
|
572
|
+
config = load_config(args.config)
|
|
573
|
+
|
|
574
|
+
if args.command is None:
|
|
575
|
+
# Interactive REPL mode
|
|
576
|
+
repl = DevHiveREPL(config)
|
|
577
|
+
try:
|
|
578
|
+
asyncio.run(repl.start())
|
|
579
|
+
except KeyboardInterrupt:
|
|
580
|
+
pass
|
|
581
|
+
elif args.command == "do":
|
|
582
|
+
asyncio.run(cmd_do(args, config))
|
|
583
|
+
elif args.command == "run":
|
|
584
|
+
asyncio.run(cmd_run(args, config))
|
|
585
|
+
elif args.command == "status":
|
|
586
|
+
asyncio.run(cmd_status(args, config))
|
|
587
|
+
elif args.command == "log":
|
|
588
|
+
asyncio.run(cmd_log(args, config))
|
|
589
|
+
elif args.command == "review":
|
|
590
|
+
asyncio.run(cmd_review(args, config))
|
|
591
|
+
elif args.command == "resolve":
|
|
592
|
+
asyncio.run(cmd_resolve(args, config))
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
if __name__ == "__main__":
|
|
596
|
+
main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""DevHive Dashboard — terminal UI for monitoring agent activity."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Dashboard:
|
|
8
|
+
"""Simple terminal-based status dashboard."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, checkpoint_store):
|
|
11
|
+
self.store = checkpoint_store
|
|
12
|
+
self._last_refresh = 0
|
|
13
|
+
|
|
14
|
+
def render(self) -> str:
|
|
15
|
+
"""Render the dashboard to a string."""
|
|
16
|
+
self._last_refresh = time.monotonic()
|
|
17
|
+
|
|
18
|
+
pending = self.store.get_pending_tasks()
|
|
19
|
+
escalations = self.store.get_open_escalations()
|
|
20
|
+
|
|
21
|
+
lines = []
|
|
22
|
+
lines.append("=" * 60)
|
|
23
|
+
lines.append(" DevHive Dashboard")
|
|
24
|
+
lines.append("=" * 60)
|
|
25
|
+
lines.append("")
|
|
26
|
+
lines.append(f" Pending Tasks: {len(pending)}")
|
|
27
|
+
lines.append(f" Open Escalations: {len(escalations)}")
|
|
28
|
+
lines.append("")
|
|
29
|
+
|
|
30
|
+
if escalations:
|
|
31
|
+
lines.append(" ── Escalations ──")
|
|
32
|
+
for esc in escalations:
|
|
33
|
+
lines.append(f" ! {esc['id'][:20]}... | Task: {esc['task_id'][:20]}...")
|
|
34
|
+
|
|
35
|
+
if pending:
|
|
36
|
+
lines.append(" ── Pending Tasks ──")
|
|
37
|
+
for t in pending[:10]:
|
|
38
|
+
lines.append(f" • {t['id'][:20]}... | Stage: {t.get('current_stage', '?')}")
|
|
39
|
+
|
|
40
|
+
lines.append("")
|
|
41
|
+
lines.append(" Press Ctrl+C to exit")
|
|
42
|
+
lines.append("-" * 60)
|
|
43
|
+
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
async def run_interactive(self, interval: float = 2.0):
|
|
47
|
+
"""Run an interactive dashboard loop."""
|
|
48
|
+
import asyncio
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
while True:
|
|
53
|
+
os.system("clear" if os.name != "nt" else "cls")
|
|
54
|
+
print(self.render())
|
|
55
|
+
await asyncio.sleep(interval)
|
|
56
|
+
except KeyboardInterrupt:
|
|
57
|
+
print("\nDashboard stopped.")
|