@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.
Files changed (2) hide show
  1. package/control_plane/cli.py +210 -490
  2. package/package.json +1 -1
@@ -3,29 +3,19 @@
3
3
  dh — DevHive CLI
4
4
 
5
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
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 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
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 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
- """
34
+ from storage.checkpoint import CheckpointStore
64
35
 
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
- """
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(config_path: str = None) -> dict:
90
- """Load YAML config, resolving env vars."""
47
+ def load_config(path: str = None) -> dict:
91
48
  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
- ]
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.exists(p):
52
+ if p and os.path.isfile(p):
100
53
  with open(p) as f:
101
54
  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)
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
- # ── Natural Language Task Parsing ────────────────────────────
60
+ # ── Task parsing ────────────────────────────────────────────
112
61
 
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."""
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
- 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
- """
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=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
- # 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
-
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
- # ── Interactive REPL ────────────────────────────────────────
87
+ # ── REPL ────────────────────────────────────────────────────
184
88
 
185
- class DevHiveREPL:
186
- """Interactive REPL with live dashboard."""
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.console = Console()
191
- self.orchestrator: Optional[Orchestrator] = None
192
- self.tui = DevHiveTUI()
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
- """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()
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
- 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:
121
+ while True:
264
122
  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):
123
+ cmd = await asyncio.get_event_loop().run_in_executor(
124
+ None, lambda: input("dh> "))
125
+ except (EOFError, KeyboardInterrupt):
277
126
  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
127
+ if cmd:
128
+ await self._dispatch(cmd.strip())
129
+ finally:
130
+ await self.orch.stop()
286
131
 
287
- async def _handle_command(self, cmd: str):
288
- """Dispatch a REPL command."""
132
+ async def _dispatch(self, cmd: str):
289
133
  # Shell escape
290
134
  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}")
135
+ self._shell(cmd[1:].strip())
304
136
  return
305
137
 
306
- # Colon commands
138
+ # Colon command or bare text
307
139
  if cmd.startswith(":"):
308
140
  parts = shlex.split(cmd)
309
- cmd_name = parts[0][1:].lower() if parts else ""
310
- args = parts[1:] if len(parts) > 1 else []
141
+ name = parts[0][1:].lower()
142
+ args = parts[1:]
311
143
  else:
312
- # Bare text → submit as task
313
- cmd_name = "do"
144
+ name = "do"
314
145
  args = [cmd]
315
146
 
316
- match cmd_name:
317
- case "help" | "h":
318
- self.tui.show(
319
- "── Help ──",
320
- "",
321
- " :do, :d <desc> Submit task from description",
322
- " :status, :st Show system status",
323
- " :log, :l <id> Show task timeline",
324
- " :review, :rv Review escalations",
325
- " :resolve, :rs <id> Resolve escalation",
326
- " :tasks, :t List all tasks",
327
- " :watch <id> Zoom into a task",
328
- " :quit, :q Exit",
329
- "",
330
- " !<cmd> Escape to shell",
331
- "",
332
- " Bare text auto-submit as task",
333
- )
334
- self.tui.log_activity("system", "Showed help")
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 parse_task_from_nl(description, config=self.config)
379
- task_id = await self.orchestrator.submit_task(spec)
380
- self.tui.update_task(task_id, "SPECIFY", "parsed",
381
- spec.title)
382
- self.tui.log_activity("task", f"Created {task_id[-16:]}: {spec.title}")
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
- self.tui.log_activity("error", f"Failed: {e}")
391
-
392
- async def _cmd_show_status(self):
393
- """Show system status."""
394
- store = self.orchestrator.checkpoint
395
- tasks = store.get_pending_tasks()
396
- escs = store.get_open_escalations()
397
- lines = [f"Active tasks: {len(tasks)}", f"Open escalations: {len(escs)}"]
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
- lines.append(f" ⚠ {e.get('id', '?')[:30]}")
400
- self.tui.show(*lines)
190
+ console.print(f" [red][/] {e.get('id','?')[:30]} task: {e.get('task_id','?')[-20:]}")
191
+ console.print()
401
192
 
402
- async def _cmd_list_tasks(self):
403
- """List all tasks in the TUI detail panel."""
404
- store = self.orchestrator.checkpoint
405
- tasks = store.get_pending_tasks()
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
- self.tui.update_task(t["id"], stage, t.get("status", ""),
409
- t.get("spec_json", "{}")[:80])
410
- self.tui.log_activity("system", f"Loaded {len(tasks)} tasks")
411
- self.tui.show(f"Loaded {len(tasks)} tasks see detail panel")
412
-
413
- async def _cmd_show_log(self, task_id: str = None):
414
- """Show task execution timeline."""
415
- if not task_id:
416
- task_id = self.tui._selected_task
417
- if not task_id:
418
- self.tui.show("Usage: :log <task-id>")
419
- return
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
- store = self.orchestrator.checkpoint
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
- console.print(" Parsing task description...", style="dim")
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(" Usage: dh do <description> or dh do -f <file>", style="red")
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
- task_id = await orch.submit_task(spec)
482
- console.print(f"\n ✓ Task submitted: {task_id}", style="green")
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.3)
500
-
501
- tasks = orch.checkpoint.get_pending_tasks()
502
- escs = orch.checkpoint.get_open_escalations()
503
- status_display(tasks, escs)
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
- task_timeline(history)
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
- selected = escalation_review(escs)
526
-
527
- if selected:
528
- orch.checkpoint.resolve_escalation(selected, "human-operator")
529
- Console().print(f"\n Resolved", style="green")
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-operator")
539
- Console().print(f" ✓ Resolved {args.escalation_id}", style="green")
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("Daemon running. Press Ctrl+C to stop.", style="green")
554
- console.print()
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
- prog="dh",
568
- description="DevHive — Multi-Agent Software Development System",
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
- # dh (no args) interactive REPL
576
- # (handled by checking if command is None below)
577
-
578
- # dh do
579
- p_do = sub.add_parser("do", help="Submit a task")
580
- p_do.add_argument("description", nargs="?", help="Task description in natural language")
581
- p_do.add_argument("-f", "--file", help="Task spec JSON/YAML file")
582
- p_do.add_argument("-p", "--priority", choices=["CRITICAL", "HIGH", "MEDIUM", "LOW"])
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 is None:
607
- # Interactive REPL mode
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oswaldzsh/devhive",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Multi-Agent Software Development System — autonomous coding with verify-specialized agents, failure signatures, and structured handoff protocols.",
5
5
  "keywords": [
6
6
  "ai",