@oswaldzsh/devhive 0.1.2 → 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.
@@ -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,555 +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)))
85
+
181
86
 
87
+ # ── REPL ────────────────────────────────────────────────────
182
88
 
183
- # ── Interactive REPL ────────────────────────────────────────
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
99
+
100
+ [bold]!<cmd>[/] Escape to shell
101
+ [dim]Bare text → auto-submit as task[/]
102
+ """
184
103
 
185
- class DevHiveREPL:
186
- """Interactive REPL with live dashboard."""
187
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.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)
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}[/]")
353
167
 
354
- async def _cmd_submit(self, description: str):
355
- """Submit a new task."""
168
+ async def _do(self, description: str):
356
169
  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
-
170
+ console.print("[red]Usage: :do <description>[/]"); return
171
+ console.print(f"[dim]Parsing...[/]")
362
172
  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")
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"))
369
178
  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()
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)
189
+ for e in escs[:5]:
190
+ console.print(f" [red]⚠[/] {e.get('id','?')[:30]} task: {e.get('task_id','?')[-20:]}")
191
+ console.print()
192
+
193
+ async def _tasks(self):
194
+ tasks = self.store.get_pending_tasks()
195
+ if not tasks:
196
+ console.print("[dim]No pending tasks.[/]"); return
383
197
  for t in tasks:
384
198
  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")
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
417
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]}[/]")
418
233
 
419
- # ── One-shot Commands ───────────────────────────────────────
420
234
 
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")
235
+ # ── One-shot commands ───────────────────────────────────────
425
236
 
237
+ async def cmd_do(args, config):
238
+ console.print(BANNER)
426
239
  if args.file:
427
240
  import yaml
428
241
  with open(args.file) as f:
429
242
  data = json.load(f) if args.file.endswith(".json") else yaml.safe_load(f)
430
243
  spec = TaskSpec(**data)
431
- console.print(f" Loaded spec from {args.file}", style="dim")
432
244
  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")
245
+ spec = await parse_task(args.description, args.priority or "MEDIUM", config)
441
246
  else:
442
- console.print(" Usage: dh do <description> or dh do -f <file>", style="red")
443
- return
444
-
247
+ console.print("[red]Usage: dh do <description> or dh do -f <file>[/]"); return
445
248
  orch = Orchestrator(config)
446
249
  await orch.start(agent_counts={"execute": 1})
447
250
  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
-
251
+ tid = await orch.submit_task(spec)
252
+ console.print(f"[green]✓ Submitted: {tid}[/]")
253
+ console.print(f" Title: {spec.title}")
460
254
  await orch.stop()
461
255
 
462
-
463
- async def cmd_status(args, config: dict):
464
- """Quick status check."""
256
+ async def cmd_status(args, config):
465
257
  orch = Orchestrator(config)
466
258
  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
-
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]}")
473
266
  await orch.stop()
474
267
 
475
-
476
- async def cmd_log(args, config: dict):
477
- """Show task timeline."""
268
+ async def cmd_log(args, config):
478
269
  orch = Orchestrator(config)
479
270
  await orch.start(agent_counts={})
480
-
481
271
  history = orch.checkpoint.get_task_history(args.task_id)
482
- task_timeline(history)
483
-
272
+ for h in history:
273
+ console.print(f" [{h['stage']}] {h['outcome']} [dim]{h['created_at']}[/]")
484
274
  await orch.stop()
485
275
 
486
-
487
- async def cmd_review(args, config: dict):
488
- """Interactive escalation review."""
276
+ async def cmd_review(args, config):
489
277
  orch = Orchestrator(config)
490
278
  await orch.start(agent_counts={})
491
-
492
279
  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
-
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
499
292
  await orch.stop()
500
293
 
501
-
502
- async def cmd_resolve(args, config: dict):
503
- """Resolve an escalation."""
294
+ async def cmd_resolve(args, config):
504
295
  orch = Orchestrator(config)
505
296
  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")
297
+ orch.checkpoint.resolve_escalation(args.escalation_id, "human")
298
+ console.print(f"[green]✓ Resolved[/]")
508
299
  await orch.stop()
509
300
 
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
-
301
+ async def cmd_run(args, config):
519
302
  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()
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()
529
307
 
530
308
 
531
- # ── Entry Point ─────────────────────────────────────────────
309
+ # ── Entry point ─────────────────────────────────────────────
532
310
 
533
311
  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}")
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}")
541
315
  sub = parser.add_subparsers(dest="command")
542
316
 
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")
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")
570
325
 
571
326
  args = parser.parse_args()
572
327
  config = load_config(args.config)
573
328
 
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
329
+ if args.command == "run":
330
+ asyncio.run(cmd_run(args, config))
581
331
  elif args.command == "do":
582
332
  asyncio.run(cmd_do(args, config))
583
- elif args.command == "run":
584
- asyncio.run(cmd_run(args, config))
585
333
  elif args.command == "status":
586
334
  asyncio.run(cmd_status(args, config))
587
335
  elif args.command == "log":
@@ -590,6 +338,10 @@ def main():
590
338
  asyncio.run(cmd_review(args, config))
591
339
  elif args.command == "resolve":
592
340
  asyncio.run(cmd_resolve(args, config))
341
+ else:
342
+ repl = REPL(config)
343
+ try: asyncio.run(repl.start())
344
+ except (KeyboardInterrupt, EOFError): pass
593
345
 
594
346
 
595
347
  if __name__ == "__main__":
@@ -63,6 +63,7 @@ class DevHiveTUI:
63
63
  self._tasks: dict = {}
64
64
  self._selected_task: Optional[str] = None
65
65
  self._running = False
66
+ self._output_lines: list[str] = [] # command output shown in detail pane
66
67
 
67
68
  def log_activity(self, source: str, message: str):
68
69
  """Add an entry to the activity feed."""
@@ -78,6 +79,10 @@ class DevHiveTUI:
78
79
  def set_tasks(self, tasks: dict):
79
80
  self._tasks = tasks
80
81
 
82
+ def show(self, *lines: str):
83
+ """Set output lines to display in the detail panel. Auto-clears after next update."""
84
+ self._output_lines = list(lines)
85
+
81
86
  @property
82
87
  def uptime(self) -> str:
83
88
  seconds = int(time.monotonic() - self._start_time)
@@ -158,7 +163,15 @@ class DevHiveTUI:
158
163
  title_align="left")
159
164
 
160
165
  def _detail_panel(self) -> Panel:
161
- """Detail view for selected or most recent task."""
166
+ """Detail view for selected task, output, or command results."""
167
+
168
+ # Output mode: show command output when available
169
+ if self._output_lines:
170
+ text = Text("\n").join(Text(line) for line in self._output_lines)
171
+ panel = Panel(text, title="Output", border_style="bright_cyan",
172
+ title_align="left")
173
+ return panel
174
+
162
175
  if self._selected_task and self._selected_task in self._tasks:
163
176
  tasks_to_show = {self._selected_task: self._tasks[self._selected_task]}
164
177
  elif self._tasks:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oswaldzsh/devhive",
3
- "version": "0.1.2",
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",