@oswaldzsh/devhive 0.1.0

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