@oswaldzsh/devhive 0.1.3 → 0.1.4
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/control_plane/cli.py +210 -490
- package/package.json +1 -1
package/control_plane/cli.py
CHANGED
|
@@ -3,29 +3,19 @@
|
|
|
3
3
|
dh — DevHive CLI
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
|
-
dh Start interactive REPL
|
|
7
|
-
dh do <description> Submit a task
|
|
8
|
-
dh do -f <file> Submit a task from JSON/YAML spec file
|
|
9
|
-
dh run Start orchestrator daemon
|
|
6
|
+
dh Start interactive REPL
|
|
7
|
+
dh do <description> Submit a task
|
|
10
8
|
dh status Show system status
|
|
11
|
-
dh log <task-id> Show task
|
|
12
|
-
dh review
|
|
13
|
-
dh resolve <id> Resolve
|
|
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
|
|
9
|
+
dh log <task-id> Show task timeline
|
|
10
|
+
dh review Review escalations
|
|
11
|
+
dh resolve <id> Resolve escalation
|
|
23
12
|
"""
|
|
24
13
|
|
|
25
14
|
import argparse
|
|
26
15
|
import asyncio
|
|
27
16
|
import json
|
|
28
17
|
import os
|
|
18
|
+
import re
|
|
29
19
|
import shlex
|
|
30
20
|
import subprocess
|
|
31
21
|
import sys
|
|
@@ -33,587 +23,313 @@ from datetime import datetime, timezone
|
|
|
33
23
|
from pathlib import Path
|
|
34
24
|
from typing import Optional
|
|
35
25
|
|
|
36
|
-
# Rich imports
|
|
37
26
|
from rich.console import Console
|
|
38
|
-
from rich.live import Live
|
|
39
27
|
from rich.panel import Panel
|
|
40
28
|
from rich.text import Text
|
|
41
29
|
from rich.table import Table
|
|
42
30
|
from rich import box
|
|
43
31
|
|
|
44
|
-
# DevHive imports
|
|
45
32
|
from protocol.schemas import TaskSpec, Priority
|
|
46
33
|
from orchestrator.engine import Orchestrator
|
|
47
|
-
from
|
|
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
|
-
"""
|
|
34
|
+
from storage.checkpoint import CheckpointStore
|
|
64
35
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
"""
|
|
36
|
+
VERSION = "0.1.4"
|
|
37
|
+
console = Console()
|
|
38
|
+
|
|
39
|
+
BANNER = """
|
|
40
|
+
[bold cyan]DevHive[/] — Multi-Agent Software Development
|
|
41
|
+
Version {version} │ :help for commands │ Ctrl+C to quit
|
|
42
|
+
""".format(version=VERSION)
|
|
85
43
|
|
|
86
44
|
|
|
87
45
|
# ── Config ──────────────────────────────────────────────────
|
|
88
46
|
|
|
89
|
-
def load_config(
|
|
90
|
-
"""Load YAML config, resolving env vars."""
|
|
47
|
+
def load_config(path: str = None) -> dict:
|
|
91
48
|
import yaml
|
|
92
|
-
paths = [
|
|
93
|
-
|
|
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
|
-
]
|
|
49
|
+
paths = [path, "config.yaml", "config.local.yaml",
|
|
50
|
+
os.path.expanduser("~/.config/devhive/config.yaml")]
|
|
98
51
|
for p in paths:
|
|
99
|
-
if p and os.path.
|
|
52
|
+
if p and os.path.isfile(p):
|
|
100
53
|
with open(p) as f:
|
|
101
54
|
raw = f.read()
|
|
102
|
-
|
|
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)
|
|
55
|
+
raw = re.sub(r'\$\{(\w+)\}', lambda m: os.environ.get(m.group(1), m.group(0)), raw)
|
|
107
56
|
return yaml.safe_load(raw)
|
|
108
57
|
return {}
|
|
109
58
|
|
|
110
59
|
|
|
111
|
-
# ──
|
|
60
|
+
# ── Task parsing ────────────────────────────────────────────
|
|
112
61
|
|
|
113
|
-
async def
|
|
114
|
-
config: dict = None) -> TaskSpec:
|
|
115
|
-
"""Use the model to parse a natural language description into a TaskSpec."""
|
|
62
|
+
async def parse_task(description: str, priority: str = "MEDIUM", config: dict = None) -> TaskSpec:
|
|
116
63
|
from tools.api_client import APIClient, extract_text_from_response
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
"""
|
|
64
|
+
cfg = (config or {}).get("api", {})
|
|
65
|
+
client = APIClient(base_url=cfg.get("base_url"), auth_token=cfg.get("auth_token"),
|
|
66
|
+
default_model=cfg.get("default_model"))
|
|
147
67
|
|
|
148
68
|
response = await client.create_message(
|
|
149
|
-
system=
|
|
69
|
+
system="Convert user request to JSON task spec. Output ONLY valid JSON, no explanation.\n"
|
|
70
|
+
'Format: {"title":"...","description":"...","acceptance_criteria":[...],'
|
|
71
|
+
'"scope_constraints":[...],"sensitive_modules":[...],"priority":"MEDIUM"}',
|
|
150
72
|
messages=[{"role": "user", "content": f"Priority: {priority}\n\n{description}"}],
|
|
151
|
-
max_tokens=1024,
|
|
152
|
-
temperature=0.1,
|
|
153
|
-
)
|
|
73
|
+
max_tokens=1024, temperature=0.1)
|
|
154
74
|
|
|
155
75
|
text = extract_text_from_response(response)
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
76
|
+
m = re.search(r'\{.*\}', text, re.DOTALL)
|
|
77
|
+
data = json.loads(m.group(0)) if m else {"title": description[:80], "description": description}
|
|
173
78
|
return TaskSpec(
|
|
174
79
|
title=data.get("title", description[:80]),
|
|
175
80
|
description=data.get("description", description),
|
|
176
81
|
acceptance_criteria=data.get("acceptance_criteria", []),
|
|
177
82
|
scope_constraints=data.get("scope_constraints", []),
|
|
178
83
|
sensitive_modules=data.get("sensitive_modules", []),
|
|
179
|
-
priority=Priority(data.get("priority", priority))
|
|
180
|
-
)
|
|
84
|
+
priority=Priority(data.get("priority", priority)))
|
|
181
85
|
|
|
182
86
|
|
|
183
|
-
# ──
|
|
87
|
+
# ── REPL ────────────────────────────────────────────────────
|
|
184
88
|
|
|
185
|
-
|
|
186
|
-
|
|
89
|
+
HELP = """
|
|
90
|
+
[bold]:do, :d <desc>[/] Submit task from natural language
|
|
91
|
+
[bold]:status, :st[/] Show pending tasks & escalations
|
|
92
|
+
[bold]:tasks, :t[/] List all tasks
|
|
93
|
+
[bold]:log, :l <id>[/] Show task execution timeline
|
|
94
|
+
[bold]:review, :rv[/] Review and resolve escalations
|
|
95
|
+
[bold]:resolve, :rs <id>[/] Resolve an escalation by ID
|
|
96
|
+
[bold]:watch <id>[/] Zoom into a specific task
|
|
97
|
+
[bold]:help, :h[/] Show this help
|
|
98
|
+
[bold]:quit, :q[/] Exit
|
|
187
99
|
|
|
100
|
+
[bold]!<cmd>[/] Escape to shell
|
|
101
|
+
[dim]Bare text → auto-submit as task[/]
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class REPL:
|
|
188
106
|
def __init__(self, config: dict):
|
|
189
107
|
self.config = config
|
|
190
|
-
self.
|
|
191
|
-
self.
|
|
192
|
-
self.
|
|
193
|
-
self._running = False
|
|
194
|
-
self._command_task: Optional[asyncio.Task] = None
|
|
108
|
+
self.orch: Optional[Orchestrator] = None
|
|
109
|
+
self.store: Optional[CheckpointStore] = None
|
|
110
|
+
self.selected_task: Optional[str] = None
|
|
195
111
|
|
|
196
112
|
async def start(self):
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
self.
|
|
200
|
-
|
|
201
|
-
self.
|
|
202
|
-
|
|
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()
|
|
113
|
+
console.print(BANNER)
|
|
114
|
+
|
|
115
|
+
self.orch = Orchestrator(self.config)
|
|
116
|
+
self.store = self.orch.checkpoint
|
|
117
|
+
asyncio.create_task(self.orch.start(agent_counts={"execute": 1}))
|
|
118
|
+
await asyncio.sleep(0.5)
|
|
119
|
+
|
|
241
120
|
try:
|
|
242
|
-
|
|
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:
|
|
121
|
+
while True:
|
|
264
122
|
try:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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):
|
|
123
|
+
cmd = await asyncio.get_event_loop().run_in_executor(
|
|
124
|
+
None, lambda: input("dh> "))
|
|
125
|
+
except (EOFError, KeyboardInterrupt):
|
|
277
126
|
break
|
|
278
|
-
|
|
279
|
-
self.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
try:
|
|
283
|
-
await refresh_task
|
|
284
|
-
except asyncio.CancelledError:
|
|
285
|
-
pass
|
|
127
|
+
if cmd:
|
|
128
|
+
await self._dispatch(cmd.strip())
|
|
129
|
+
finally:
|
|
130
|
+
await self.orch.stop()
|
|
286
131
|
|
|
287
|
-
async def
|
|
288
|
-
"""Dispatch a REPL command."""
|
|
132
|
+
async def _dispatch(self, cmd: str):
|
|
289
133
|
# Shell escape
|
|
290
134
|
if cmd.startswith("!"):
|
|
291
|
-
|
|
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}")
|
|
135
|
+
self._shell(cmd[1:].strip())
|
|
304
136
|
return
|
|
305
137
|
|
|
306
|
-
# Colon
|
|
138
|
+
# Colon command or bare text
|
|
307
139
|
if cmd.startswith(":"):
|
|
308
140
|
parts = shlex.split(cmd)
|
|
309
|
-
|
|
310
|
-
args = parts[1:]
|
|
141
|
+
name = parts[0][1:].lower()
|
|
142
|
+
args = parts[1:]
|
|
311
143
|
else:
|
|
312
|
-
|
|
313
|
-
cmd_name = "do"
|
|
144
|
+
name = "do"
|
|
314
145
|
args = [cmd]
|
|
315
146
|
|
|
316
|
-
match
|
|
317
|
-
case "help" | "h":
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
case "quit" | "q" | "exit":
|
|
337
|
-
self._running = False
|
|
338
|
-
self.tui.log_activity("system", "Shutting down...")
|
|
339
|
-
|
|
340
|
-
case "do" | "d":
|
|
341
|
-
await self._cmd_submit(" ".join(args) if args else "")
|
|
342
|
-
|
|
343
|
-
case "status" | "st":
|
|
344
|
-
await self._cmd_show_status()
|
|
345
|
-
|
|
346
|
-
case "tasks" | "t":
|
|
347
|
-
await self._cmd_list_tasks()
|
|
348
|
-
|
|
349
|
-
case "log" | "l":
|
|
350
|
-
await self._cmd_show_log(args[0] if args else None)
|
|
351
|
-
|
|
352
|
-
case "review" | "rv":
|
|
353
|
-
await self._cmd_review()
|
|
354
|
-
|
|
355
|
-
case "resolve" | "rs":
|
|
356
|
-
if args:
|
|
357
|
-
await self._cmd_resolve(args[0])
|
|
358
|
-
|
|
359
|
-
case "watch" | "w":
|
|
360
|
-
if args:
|
|
361
|
-
self.tui._selected_task = args[0]
|
|
362
|
-
self.tui.log_activity("system", f"Watching task {args[0]}")
|
|
363
|
-
|
|
364
|
-
case _:
|
|
365
|
-
# Unknown command → treat as task description
|
|
366
|
-
desc = cmd[1:] if cmd.startswith(":") else cmd
|
|
367
|
-
await self._cmd_submit(desc)
|
|
368
|
-
|
|
369
|
-
async def _cmd_submit(self, description: str):
|
|
370
|
-
"""Submit a new task."""
|
|
371
|
-
if not description:
|
|
372
|
-
self.tui.log_activity("error", "Usage: :do <description>")
|
|
373
|
-
return
|
|
374
|
-
|
|
375
|
-
self.tui.log_activity("system", f"Parsing: {description[:60]}...")
|
|
147
|
+
match name:
|
|
148
|
+
case "help" | "h": console.print(HELP)
|
|
149
|
+
case "quit" | "q" | "exit": raise EOFError
|
|
150
|
+
case "do" | "d": await self._do(" ".join(args))
|
|
151
|
+
case "status" | "st": await self._status()
|
|
152
|
+
case "tasks" | "t": await self._tasks()
|
|
153
|
+
case "log" | "l": await self._log(args[0] if args else None)
|
|
154
|
+
case "review" | "rv": await self._review()
|
|
155
|
+
case "resolve" | "rs": await self._resolve(args[0] if args else "")
|
|
156
|
+
case "watch" | "w": self.selected_task = args[0] if args else self.selected_task
|
|
157
|
+
case _: await self._do(cmd if not cmd.startswith(":") else cmd[1:])
|
|
158
|
+
|
|
159
|
+
def _shell(self, cmd: str):
|
|
160
|
+
if not cmd: return
|
|
161
|
+
try:
|
|
162
|
+
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
|
163
|
+
if r.stdout: console.print(r.stdout.rstrip(), style="dim")
|
|
164
|
+
if r.stderr: console.print(r.stderr.rstrip(), style="red")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
console.print(f"[red]Error: {e}[/]")
|
|
376
167
|
|
|
168
|
+
async def _do(self, description: str):
|
|
169
|
+
if not description:
|
|
170
|
+
console.print("[red]Usage: :do <description>[/]"); return
|
|
171
|
+
console.print(f"[dim]Parsing...[/]")
|
|
377
172
|
try:
|
|
378
|
-
spec = await
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
self.tui.show(
|
|
384
|
-
f"✓ Task created: {task_id}",
|
|
385
|
-
f" Title: {spec.title}",
|
|
386
|
-
f" Priority: {spec.priority.value}",
|
|
387
|
-
*([f" Criteria: {c}" for c in spec.acceptance_criteria[:3]] if spec.acceptance_criteria else []),
|
|
388
|
-
)
|
|
173
|
+
spec = await parse_task(description, config=self.config)
|
|
174
|
+
tid = await self.orch.submit_task(spec)
|
|
175
|
+
console.print(Panel(
|
|
176
|
+
f"Title: {spec.title}\nPriority: {spec.priority.value}\nID: {tid}",
|
|
177
|
+
title="[green]✓ Task Created[/]", border_style="green"))
|
|
389
178
|
except Exception as e:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
async def
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
179
|
+
console.print(f"[red]Failed: {e}[/]")
|
|
180
|
+
|
|
181
|
+
async def _status(self):
|
|
182
|
+
tasks = self.store.get_pending_tasks()
|
|
183
|
+
escs = self.store.get_open_escalations()
|
|
184
|
+
t = Table(title="Status", box=box.ROUNDED)
|
|
185
|
+
t.add_column("Metric"); t.add_column("Count")
|
|
186
|
+
t.add_row("Pending tasks", str(len(tasks)))
|
|
187
|
+
t.add_row("Open escalations", str(len(escs)))
|
|
188
|
+
console.print(t)
|
|
398
189
|
for e in escs[:5]:
|
|
399
|
-
|
|
400
|
-
|
|
190
|
+
console.print(f" [red]⚠[/] {e.get('id','?')[:30]} task: {e.get('task_id','?')[-20:]}")
|
|
191
|
+
console.print()
|
|
401
192
|
|
|
402
|
-
async def
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
193
|
+
async def _tasks(self):
|
|
194
|
+
tasks = self.store.get_pending_tasks()
|
|
195
|
+
if not tasks:
|
|
196
|
+
console.print("[dim]No pending tasks.[/]"); return
|
|
406
197
|
for t in tasks:
|
|
407
198
|
stage = t.get("current_stage", "?")
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if not
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
199
|
+
console.print(f" • {t['id'][-20:]} [{stage}] {t.get('status','pending')}")
|
|
200
|
+
|
|
201
|
+
async def _log(self, tid: str = None):
|
|
202
|
+
tid = tid or self.selected_task
|
|
203
|
+
if not tid:
|
|
204
|
+
console.print("[red]Usage: :log <task-id>[/]"); return
|
|
205
|
+
history = self.store.get_task_history(tid)
|
|
206
|
+
if not history:
|
|
207
|
+
console.print("[dim]No history found.[/]"); return
|
|
208
|
+
console.print(f"[bold]── {tid} ──[/]")
|
|
209
|
+
for h in history:
|
|
210
|
+
icon = "✓" if h["outcome"] in ("COMPLETED","PASS") else "✗" if h["outcome"]=="FAIL" else "●"
|
|
211
|
+
console.print(f" {icon} [{h['stage']}] {h['outcome']} [dim]{h['created_at']}[/]")
|
|
212
|
+
|
|
213
|
+
async def _review(self):
|
|
214
|
+
escs = self.store.get_open_escalations()
|
|
215
|
+
if not escs:
|
|
216
|
+
console.print("[green]No open escalations![/]"); return
|
|
217
|
+
for i, e in enumerate(escs):
|
|
218
|
+
console.print(f" [{i+1}] {e['id'][:30]}\n task: {e.get('task_id','?')[-30:]}\n {e.get('created_at','?')}")
|
|
219
|
+
try:
|
|
220
|
+
ch = input(" Select [1-N] or Enter to skip: ").strip()
|
|
221
|
+
if ch.isdigit():
|
|
222
|
+
idx = int(ch) - 1
|
|
223
|
+
if 0 <= idx < len(escs):
|
|
224
|
+
self.store.resolve_escalation(escs[idx]["id"], "human")
|
|
225
|
+
console.print(f"[green]✓ Resolved[/]")
|
|
226
|
+
except (EOFError, KeyboardInterrupt): pass
|
|
227
|
+
|
|
228
|
+
async def _resolve(self, eid: str):
|
|
229
|
+
if not eid:
|
|
230
|
+
console.print("[red]Usage: :resolve <escalation-id>[/]"); return
|
|
231
|
+
self.store.resolve_escalation(eid, "human")
|
|
232
|
+
console.print(f"[green]✓ Resolved {eid[:30]}[/]")
|
|
233
|
+
|
|
420
234
|
|
|
421
|
-
|
|
422
|
-
history = store.get_task_history(task_id)
|
|
423
|
-
|
|
424
|
-
lines = [f"── Timeline: {task_id[-20:]} ──"]
|
|
425
|
-
for cp in history:
|
|
426
|
-
stage = cp.get("stage", "?")
|
|
427
|
-
outcome = cp.get("outcome", "?")
|
|
428
|
-
created = cp.get("created_at", "?")
|
|
429
|
-
icon = "+" if outcome in ("COMPLETED", "PASS") else "x" if outcome == "FAIL" else "~"
|
|
430
|
-
lines.append(f" [{created}] {icon} {stage} → {outcome}")
|
|
431
|
-
self.tui.show(*lines)
|
|
432
|
-
|
|
433
|
-
async def _cmd_review(self):
|
|
434
|
-
"""Interactive escalation review."""
|
|
435
|
-
store = self.orchestrator.checkpoint
|
|
436
|
-
escs = store.get_open_escalations()
|
|
437
|
-
selected = escalation_review(escs)
|
|
438
|
-
if selected:
|
|
439
|
-
self.console.print(f"\n Resolving {selected}...")
|
|
440
|
-
store.resolve_escalation(selected, "human-operator")
|
|
441
|
-
self.tui.log_activity("system", f"Resolved escalation {selected[:20]}...")
|
|
442
|
-
self.console.print(" ✓ Resolved", style="green")
|
|
443
|
-
|
|
444
|
-
async def _cmd_resolve(self, esc_id: str):
|
|
445
|
-
"""Resolve an escalation by ID."""
|
|
446
|
-
self.orchestrator.checkpoint.resolve_escalation(esc_id, "human-operator")
|
|
447
|
-
self.tui.log_activity("system", f"Resolved {esc_id[:20]}...")
|
|
448
|
-
self.console.print(f" ✓ Escalation resolved", style="green")
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
# ── One-shot Commands ───────────────────────────────────────
|
|
452
|
-
|
|
453
|
-
async def cmd_do(args, config: dict):
|
|
454
|
-
"""Quick task submission from the command line."""
|
|
455
|
-
console = Console()
|
|
456
|
-
console.print(BANNER, style="bold cyan")
|
|
235
|
+
# ── One-shot commands ───────────────────────────────────────
|
|
457
236
|
|
|
237
|
+
async def cmd_do(args, config):
|
|
238
|
+
console.print(BANNER)
|
|
458
239
|
if args.file:
|
|
459
240
|
import yaml
|
|
460
241
|
with open(args.file) as f:
|
|
461
242
|
data = json.load(f) if args.file.endswith(".json") else yaml.safe_load(f)
|
|
462
243
|
spec = TaskSpec(**data)
|
|
463
|
-
console.print(f" Loaded spec from {args.file}", style="dim")
|
|
464
244
|
elif args.description:
|
|
465
|
-
|
|
466
|
-
spec = await parse_task_from_nl(
|
|
467
|
-
args.description,
|
|
468
|
-
priority=args.priority or "MEDIUM",
|
|
469
|
-
config=config,
|
|
470
|
-
)
|
|
471
|
-
console.print(f" Title: {spec.title}", style="bold")
|
|
472
|
-
console.print(f" Priority: {spec.priority.value}", style="dim")
|
|
245
|
+
spec = await parse_task(args.description, args.priority or "MEDIUM", config)
|
|
473
246
|
else:
|
|
474
|
-
console.print("
|
|
475
|
-
return
|
|
476
|
-
|
|
247
|
+
console.print("[red]Usage: dh do <description> or dh do -f <file>[/]"); return
|
|
477
248
|
orch = Orchestrator(config)
|
|
478
249
|
await orch.start(agent_counts={"execute": 1})
|
|
479
250
|
await asyncio.sleep(0.3)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
console.print(f"
|
|
483
|
-
|
|
484
|
-
# Wait briefly for execution to start
|
|
485
|
-
await asyncio.sleep(2)
|
|
486
|
-
|
|
487
|
-
# Show task status
|
|
488
|
-
history = orch.checkpoint.get_task_history(task_id)
|
|
489
|
-
if history:
|
|
490
|
-
task_timeline(history)
|
|
491
|
-
|
|
251
|
+
tid = await orch.submit_task(spec)
|
|
252
|
+
console.print(f"[green]✓ Submitted: {tid}[/]")
|
|
253
|
+
console.print(f" Title: {spec.title}")
|
|
492
254
|
await orch.stop()
|
|
493
255
|
|
|
494
|
-
|
|
495
|
-
async def cmd_status(args, config: dict):
|
|
496
|
-
"""Quick status check."""
|
|
256
|
+
async def cmd_status(args, config):
|
|
497
257
|
orch = Orchestrator(config)
|
|
498
258
|
await orch.start(agent_counts={})
|
|
499
|
-
await asyncio.sleep(0.
|
|
500
|
-
|
|
501
|
-
tasks =
|
|
502
|
-
escs =
|
|
503
|
-
|
|
504
|
-
|
|
259
|
+
await asyncio.sleep(0.2)
|
|
260
|
+
store = orch.checkpoint
|
|
261
|
+
tasks = store.get_pending_tasks()
|
|
262
|
+
escs = store.get_open_escalations()
|
|
263
|
+
console.print(f"Pending tasks: {len(tasks)} | Open escalations: {len(escs)}")
|
|
264
|
+
for e in escs:
|
|
265
|
+
console.print(f" [red]⚠[/] {e['id'][:30]}")
|
|
505
266
|
await orch.stop()
|
|
506
267
|
|
|
507
|
-
|
|
508
|
-
async def cmd_log(args, config: dict):
|
|
509
|
-
"""Show task timeline."""
|
|
268
|
+
async def cmd_log(args, config):
|
|
510
269
|
orch = Orchestrator(config)
|
|
511
270
|
await orch.start(agent_counts={})
|
|
512
|
-
|
|
513
271
|
history = orch.checkpoint.get_task_history(args.task_id)
|
|
514
|
-
|
|
515
|
-
|
|
272
|
+
for h in history:
|
|
273
|
+
console.print(f" [{h['stage']}] {h['outcome']} [dim]{h['created_at']}[/]")
|
|
516
274
|
await orch.stop()
|
|
517
275
|
|
|
518
|
-
|
|
519
|
-
async def cmd_review(args, config: dict):
|
|
520
|
-
"""Interactive escalation review."""
|
|
276
|
+
async def cmd_review(args, config):
|
|
521
277
|
orch = Orchestrator(config)
|
|
522
278
|
await orch.start(agent_counts={})
|
|
523
|
-
|
|
524
279
|
escs = orch.checkpoint.get_open_escalations()
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
280
|
+
if not escs:
|
|
281
|
+
console.print("[green]No escalations[/]")
|
|
282
|
+
else:
|
|
283
|
+
for i, e in enumerate(escs):
|
|
284
|
+
console.print(f" [{i+1}] {e['id'][:30]}")
|
|
285
|
+
try:
|
|
286
|
+
ch = input("Select: ").strip()
|
|
287
|
+
if ch.isdigit():
|
|
288
|
+
e = escs[int(ch)-1]
|
|
289
|
+
orch.checkpoint.resolve_escalation(e["id"], "human")
|
|
290
|
+
console.print("[green]✓ Resolved[/]")
|
|
291
|
+
except (EOFError, KeyboardInterrupt): pass
|
|
531
292
|
await orch.stop()
|
|
532
293
|
|
|
533
|
-
|
|
534
|
-
async def cmd_resolve(args, config: dict):
|
|
535
|
-
"""Resolve an escalation."""
|
|
294
|
+
async def cmd_resolve(args, config):
|
|
536
295
|
orch = Orchestrator(config)
|
|
537
296
|
await orch.start(agent_counts={})
|
|
538
|
-
orch.checkpoint.resolve_escalation(args.escalation_id, "human
|
|
539
|
-
|
|
297
|
+
orch.checkpoint.resolve_escalation(args.escalation_id, "human")
|
|
298
|
+
console.print(f"[green]✓ Resolved[/]")
|
|
540
299
|
await orch.stop()
|
|
541
300
|
|
|
542
|
-
|
|
543
|
-
# ── Daemon ──────────────────────────────────────────────────
|
|
544
|
-
|
|
545
|
-
async def cmd_run(args, config: dict):
|
|
546
|
-
"""Start orchestrator daemon (for long-running use)."""
|
|
547
|
-
console = Console()
|
|
548
|
-
console.print(BANNER, style="bold cyan")
|
|
549
|
-
console.print(" Starting DevHive daemon...", style="dim")
|
|
550
|
-
|
|
301
|
+
async def cmd_run(args, config):
|
|
551
302
|
orch = Orchestrator(config)
|
|
552
|
-
await orch.start(agent_counts=args.agents)
|
|
553
|
-
console.print("
|
|
554
|
-
|
|
303
|
+
await orch.start(agent_counts={"execute": args.agents or 1})
|
|
304
|
+
console.print("[green]Daemon running. Ctrl+C to stop.[/]")
|
|
305
|
+
try: await asyncio.Event().wait()
|
|
306
|
+
except KeyboardInterrupt: await orch.stop()
|
|
555
307
|
|
|
556
|
-
try:
|
|
557
|
-
await asyncio.Event().wait()
|
|
558
|
-
except KeyboardInterrupt:
|
|
559
|
-
console.print("\n Shutting down...", style="dim")
|
|
560
|
-
await orch.stop()
|
|
561
308
|
|
|
562
|
-
|
|
563
|
-
# ── Entry Point ─────────────────────────────────────────────
|
|
309
|
+
# ── Entry point ─────────────────────────────────────────────
|
|
564
310
|
|
|
565
311
|
def main():
|
|
566
|
-
parser = argparse.ArgumentParser(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
)
|
|
570
|
-
parser.add_argument("-c", "--config", help="Config file path")
|
|
571
|
-
parser.add_argument("-v", "--version", action="version",
|
|
572
|
-
version=f"DevHive {VERSION}")
|
|
312
|
+
parser = argparse.ArgumentParser(prog="dh", description="DevHive CLI")
|
|
313
|
+
parser.add_argument("-c", "--config")
|
|
314
|
+
parser.add_argument("-v", "--version", action="version", version=f"DevHive {VERSION}")
|
|
573
315
|
sub = parser.add_subparsers(dest="command")
|
|
574
316
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
# dh run
|
|
585
|
-
p_run = sub.add_parser("run", help="Start orchestrator daemon")
|
|
586
|
-
p_run.add_argument("-a", "--agents", type=int, default=1,
|
|
587
|
-
help="Number of execute agents")
|
|
588
|
-
|
|
589
|
-
# dh status
|
|
590
|
-
sub.add_parser("status", help="Show system status")
|
|
591
|
-
|
|
592
|
-
# dh log
|
|
593
|
-
p_log = sub.add_parser("log", help="Show task execution timeline")
|
|
594
|
-
p_log.add_argument("task_id", help="Task ID")
|
|
595
|
-
|
|
596
|
-
# dh review
|
|
597
|
-
sub.add_parser("review", help="Interactive escalation review")
|
|
598
|
-
|
|
599
|
-
# dh resolve
|
|
600
|
-
p_resolve = sub.add_parser("resolve", help="Resolve an escalation")
|
|
601
|
-
p_resolve.add_argument("escalation_id", help="Escalation ID")
|
|
317
|
+
p_do = sub.add_parser("do"); p_do.add_argument("description", nargs="?")
|
|
318
|
+
p_do.add_argument("-f", "--file"); p_do.add_argument("-p", "--priority",
|
|
319
|
+
choices=["CRITICAL","HIGH","MEDIUM","LOW"])
|
|
320
|
+
p_run = sub.add_parser("run"); p_run.add_argument("-a", "--agents", type=int)
|
|
321
|
+
sub.add_parser("status")
|
|
322
|
+
p_log = sub.add_parser("log"); p_log.add_argument("task_id")
|
|
323
|
+
sub.add_parser("review")
|
|
324
|
+
p_res = sub.add_parser("resolve"); p_res.add_argument("escalation_id")
|
|
602
325
|
|
|
603
326
|
args = parser.parse_args()
|
|
604
327
|
config = load_config(args.config)
|
|
605
328
|
|
|
606
|
-
if args.command
|
|
607
|
-
|
|
608
|
-
repl = DevHiveREPL(config)
|
|
609
|
-
try:
|
|
610
|
-
asyncio.run(repl.start())
|
|
611
|
-
except KeyboardInterrupt:
|
|
612
|
-
pass
|
|
329
|
+
if args.command == "run":
|
|
330
|
+
asyncio.run(cmd_run(args, config))
|
|
613
331
|
elif args.command == "do":
|
|
614
332
|
asyncio.run(cmd_do(args, config))
|
|
615
|
-
elif args.command == "run":
|
|
616
|
-
asyncio.run(cmd_run(args, config))
|
|
617
333
|
elif args.command == "status":
|
|
618
334
|
asyncio.run(cmd_status(args, config))
|
|
619
335
|
elif args.command == "log":
|
|
@@ -622,6 +338,10 @@ def main():
|
|
|
622
338
|
asyncio.run(cmd_review(args, config))
|
|
623
339
|
elif args.command == "resolve":
|
|
624
340
|
asyncio.run(cmd_resolve(args, config))
|
|
341
|
+
else:
|
|
342
|
+
repl = REPL(config)
|
|
343
|
+
try: asyncio.run(repl.start())
|
|
344
|
+
except (KeyboardInterrupt, EOFError): pass
|
|
625
345
|
|
|
626
346
|
|
|
627
347
|
if __name__ == "__main__":
|
package/package.json
CHANGED