daveloop 1.5.0__tar.gz → 1.5.1__tar.gz
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.
- {daveloop-1.5.0 → daveloop-1.5.1}/PKG-INFO +13 -2
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop.egg-info/PKG-INFO +13 -2
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop.egg-info/SOURCES.txt +1 -0
- daveloop-1.5.1/daveloop.egg-info/requires.txt +3 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop.py +475 -4
- {daveloop-1.5.0 → daveloop-1.5.1}/setup.py +5 -2
- {daveloop-1.5.0 → daveloop-1.5.1}/MANIFEST.in +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/README.md +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop.egg-info/dependency_links.txt +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop.egg-info/entry_points.txt +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop.egg-info/top_level.txt +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop_maestro_prompt.md +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop_prompt.md +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop_swebench.py +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/daveloop_web_prompt.md +0 -0
- {daveloop-1.5.0 → daveloop-1.5.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: daveloop
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Self-healing debug agent powered by Claude Code CLI
|
|
5
5
|
Home-page: https://github.com/davebruzil/DaveLoop
|
|
6
6
|
Author: Dave Bruzil
|
|
@@ -19,6 +19,17 @@ Classifier: Topic :: Software Development :: Debuggers
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
20
|
Requires-Python: >=3.7
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: turbo
|
|
23
|
+
Requires-Dist: rich>=13.0; extra == "turbo"
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: provides-extra
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
22
33
|
|
|
23
34
|
# DaveLoop
|
|
24
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: daveloop
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Self-healing debug agent powered by Claude Code CLI
|
|
5
5
|
Home-page: https://github.com/davebruzil/DaveLoop
|
|
6
6
|
Author: Dave Bruzil
|
|
@@ -19,6 +19,17 @@ Classifier: Topic :: Software Development :: Debuggers
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
20
|
Requires-Python: >=3.7
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: turbo
|
|
23
|
+
Requires-Dist: rich>=13.0; extra == "turbo"
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: provides-extra
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
22
33
|
|
|
23
34
|
# DaveLoop
|
|
24
35
|
|
|
@@ -14,6 +14,8 @@ import itertools
|
|
|
14
14
|
import json
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from collections import deque
|
|
18
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
17
19
|
|
|
18
20
|
# Configuration
|
|
19
21
|
MAX_ITERATIONS = 20
|
|
@@ -318,6 +320,386 @@ class SwarmBudget:
|
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
|
|
323
|
+
# ============================================================================
|
|
324
|
+
# Turbo Dashboard (Rich-based split-pane UI)
|
|
325
|
+
# ============================================================================
|
|
326
|
+
class TurboDashboard:
|
|
327
|
+
"""Rich-based split-pane terminal dashboard for parallel DaveLoop instances.
|
|
328
|
+
|
|
329
|
+
Shows one panel per instance with: instance ID, current task name,
|
|
330
|
+
and a scrolling window of the last ~20 lines of output.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
PANEL_COLORS = ["cyan", "green", "yellow", "magenta", "blue", "red"]
|
|
334
|
+
|
|
335
|
+
def __init__(self, instance_count: int):
|
|
336
|
+
from rich.live import Live
|
|
337
|
+
from rich.layout import Layout
|
|
338
|
+
from rich.panel import Panel
|
|
339
|
+
from rich.text import Text
|
|
340
|
+
from rich.console import Console
|
|
341
|
+
|
|
342
|
+
self._Live = Live
|
|
343
|
+
self._Layout = Layout
|
|
344
|
+
self._Panel = Panel
|
|
345
|
+
self._Text = Text
|
|
346
|
+
self._console = Console()
|
|
347
|
+
|
|
348
|
+
self.instance_count = instance_count
|
|
349
|
+
self._lock = threading.Lock()
|
|
350
|
+
|
|
351
|
+
# Per-instance state
|
|
352
|
+
self.task_names = ["Waiting..."] * instance_count
|
|
353
|
+
self.statuses = ["pending"] * instance_count # pending, running, done, failed
|
|
354
|
+
self.output_buffers = [deque(maxlen=20) for _ in range(instance_count)]
|
|
355
|
+
self.iterations = [0] * instance_count
|
|
356
|
+
self.max_iterations = [0] * instance_count
|
|
357
|
+
self.start_times = [None] * instance_count
|
|
358
|
+
|
|
359
|
+
self._live = None
|
|
360
|
+
|
|
361
|
+
def _build_layout(self):
|
|
362
|
+
"""Build the Rich Layout with panels for each instance."""
|
|
363
|
+
layout = self._Layout()
|
|
364
|
+
|
|
365
|
+
# Create rows of 2 panels each
|
|
366
|
+
rows = []
|
|
367
|
+
for i in range(0, self.instance_count, 2):
|
|
368
|
+
row_name = f"row_{i // 2}"
|
|
369
|
+
row = self._Layout(name=row_name)
|
|
370
|
+
left = self._Layout(name=f"inst_{i}")
|
|
371
|
+
|
|
372
|
+
if i + 1 < self.instance_count:
|
|
373
|
+
right = self._Layout(name=f"inst_{i + 1}")
|
|
374
|
+
row.split_row(left, right)
|
|
375
|
+
else:
|
|
376
|
+
row.split_row(left)
|
|
377
|
+
|
|
378
|
+
rows.append(row)
|
|
379
|
+
|
|
380
|
+
if rows:
|
|
381
|
+
layout.split_column(*rows)
|
|
382
|
+
|
|
383
|
+
return layout
|
|
384
|
+
|
|
385
|
+
def _render_panel(self, idx: int):
|
|
386
|
+
"""Render a single instance panel."""
|
|
387
|
+
color = self.PANEL_COLORS[idx % len(self.PANEL_COLORS)]
|
|
388
|
+
status = self.statuses[idx]
|
|
389
|
+
|
|
390
|
+
# Status indicator
|
|
391
|
+
if status == "running":
|
|
392
|
+
status_icon = "[bold bright_green]\u25cf RUNNING[/]"
|
|
393
|
+
elif status == "done":
|
|
394
|
+
status_icon = "[bold bright_green]\u2713 RESOLVED[/]"
|
|
395
|
+
elif status == "failed":
|
|
396
|
+
status_icon = "[bold bright_red]\u2717 FAILED[/]"
|
|
397
|
+
else:
|
|
398
|
+
status_icon = "[dim]\u25cb PENDING[/]"
|
|
399
|
+
|
|
400
|
+
# Iteration info
|
|
401
|
+
iter_info = ""
|
|
402
|
+
if self.max_iterations[idx] > 0:
|
|
403
|
+
iter_info = f" | Iter {self.iterations[idx]}/{self.max_iterations[idx]}"
|
|
404
|
+
|
|
405
|
+
# Elapsed time
|
|
406
|
+
elapsed = ""
|
|
407
|
+
if self.start_times[idx]:
|
|
408
|
+
secs = int(time.time() - self.start_times[idx])
|
|
409
|
+
mins, secs = divmod(secs, 60)
|
|
410
|
+
elapsed = f" | {mins}m{secs:02d}s"
|
|
411
|
+
|
|
412
|
+
# Build subtitle
|
|
413
|
+
subtitle = f"{status_icon}{iter_info}{elapsed}"
|
|
414
|
+
|
|
415
|
+
# Build output text
|
|
416
|
+
lines = list(self.output_buffers[idx])
|
|
417
|
+
if not lines:
|
|
418
|
+
lines = ["[dim]Waiting for output...[/]"]
|
|
419
|
+
|
|
420
|
+
output_text = "\n".join(lines)
|
|
421
|
+
|
|
422
|
+
task_name = self.task_names[idx]
|
|
423
|
+
if len(task_name) > 50:
|
|
424
|
+
task_name = task_name[:47] + "..."
|
|
425
|
+
|
|
426
|
+
panel = self._Panel(
|
|
427
|
+
output_text,
|
|
428
|
+
title=f"[bold {color}] Instance {idx + 1} [/] {task_name}",
|
|
429
|
+
subtitle=subtitle,
|
|
430
|
+
border_style=color if status == "running" else "dim" if status == "pending" else "green" if status == "done" else "red",
|
|
431
|
+
padding=(0, 1),
|
|
432
|
+
)
|
|
433
|
+
return panel
|
|
434
|
+
|
|
435
|
+
def _refresh(self):
|
|
436
|
+
"""Rebuild and update the live display."""
|
|
437
|
+
layout = self._build_layout()
|
|
438
|
+
for i in range(self.instance_count):
|
|
439
|
+
try:
|
|
440
|
+
layout[f"inst_{i}"].update(self._render_panel(i))
|
|
441
|
+
except KeyError:
|
|
442
|
+
pass
|
|
443
|
+
return layout
|
|
444
|
+
|
|
445
|
+
def start(self):
|
|
446
|
+
"""Start the live dashboard."""
|
|
447
|
+
self._live = self._Live(
|
|
448
|
+
self._refresh(),
|
|
449
|
+
console=self._console,
|
|
450
|
+
refresh_per_second=4,
|
|
451
|
+
screen=True,
|
|
452
|
+
)
|
|
453
|
+
self._live.start()
|
|
454
|
+
|
|
455
|
+
def stop(self):
|
|
456
|
+
"""Stop the live dashboard."""
|
|
457
|
+
if self._live:
|
|
458
|
+
self._live.stop()
|
|
459
|
+
|
|
460
|
+
def update_task(self, idx: int, task_name: str, max_iterations: int):
|
|
461
|
+
"""Set the task name and max iterations for an instance."""
|
|
462
|
+
with self._lock:
|
|
463
|
+
self.task_names[idx] = task_name
|
|
464
|
+
self.max_iterations[idx] = max_iterations
|
|
465
|
+
self.statuses[idx] = "running"
|
|
466
|
+
self.start_times[idx] = time.time()
|
|
467
|
+
if self._live:
|
|
468
|
+
self._live.update(self._refresh())
|
|
469
|
+
|
|
470
|
+
def update_iteration(self, idx: int, iteration: int):
|
|
471
|
+
"""Update the current iteration number for an instance."""
|
|
472
|
+
with self._lock:
|
|
473
|
+
self.iterations[idx] = iteration
|
|
474
|
+
if self._live:
|
|
475
|
+
self._live.update(self._refresh())
|
|
476
|
+
|
|
477
|
+
def append_output(self, idx: int, line: str):
|
|
478
|
+
"""Append a line to an instance's output buffer."""
|
|
479
|
+
with self._lock:
|
|
480
|
+
# Strip ANSI codes for cleaner display in Rich panels
|
|
481
|
+
clean = _strip_ansi(line)
|
|
482
|
+
if clean.strip():
|
|
483
|
+
self.output_buffers[idx].append(clean)
|
|
484
|
+
if self._live:
|
|
485
|
+
self._live.update(self._refresh())
|
|
486
|
+
|
|
487
|
+
def mark_done(self, idx: int, outcome: str = "done"):
|
|
488
|
+
"""Mark an instance as completed."""
|
|
489
|
+
with self._lock:
|
|
490
|
+
self.statuses[idx] = outcome
|
|
491
|
+
if self._live:
|
|
492
|
+
self._live.update(self._refresh())
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _strip_ansi(text: str) -> str:
|
|
496
|
+
"""Remove ANSI escape sequences from a string."""
|
|
497
|
+
import re
|
|
498
|
+
return re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', text)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def run_claude_code_turbo(prompt: str, working_dir: str, instance_idx: int,
|
|
502
|
+
dashboard: TurboDashboard, timeout: int = DEFAULT_TIMEOUT,
|
|
503
|
+
swarm_mode: bool = False, swarm_budget_max: int = 5,
|
|
504
|
+
swarm_depth_max: int = 1) -> str:
|
|
505
|
+
"""Execute Claude Code CLI for turbo mode, streaming output to a dashboard panel.
|
|
506
|
+
|
|
507
|
+
Similar to run_claude_code but routes output to a TurboDashboard panel
|
|
508
|
+
instead of printing directly to stdout.
|
|
509
|
+
"""
|
|
510
|
+
claude_cmd = find_claude_cli()
|
|
511
|
+
if not claude_cmd:
|
|
512
|
+
return "[DAVELOOP:ERROR] Claude CLI not found"
|
|
513
|
+
|
|
514
|
+
cmd = [claude_cmd]
|
|
515
|
+
allowed = ALLOWED_TOOLS_SWARM if swarm_mode else ALLOWED_TOOLS_DEFAULT
|
|
516
|
+
cmd.extend(["-p", "--verbose", "--output-format", "stream-json", "--allowedTools", allowed])
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
process = subprocess.Popen(
|
|
520
|
+
cmd,
|
|
521
|
+
stdin=subprocess.PIPE,
|
|
522
|
+
stdout=subprocess.PIPE,
|
|
523
|
+
stderr=subprocess.STDOUT,
|
|
524
|
+
text=True,
|
|
525
|
+
encoding='utf-8',
|
|
526
|
+
errors='replace',
|
|
527
|
+
cwd=working_dir,
|
|
528
|
+
bufsize=1
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
process.stdin.write(prompt)
|
|
532
|
+
process.stdin.close()
|
|
533
|
+
|
|
534
|
+
full_text = []
|
|
535
|
+
|
|
536
|
+
for line in process.stdout:
|
|
537
|
+
line = line.strip()
|
|
538
|
+
if not line:
|
|
539
|
+
continue
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
data = json.loads(line)
|
|
543
|
+
msg_type = data.get("type", "")
|
|
544
|
+
|
|
545
|
+
if msg_type == "assistant":
|
|
546
|
+
content = data.get("message", {}).get("content", [])
|
|
547
|
+
for block in content:
|
|
548
|
+
if block.get("type") == "text":
|
|
549
|
+
text = block.get("text", "")
|
|
550
|
+
for text_line in text.split('\n'):
|
|
551
|
+
if text_line.strip():
|
|
552
|
+
dashboard.append_output(instance_idx, text_line)
|
|
553
|
+
full_text.append(text)
|
|
554
|
+
elif block.get("type") == "tool_use":
|
|
555
|
+
tool_name = block.get("name", "unknown")
|
|
556
|
+
tool_input = block.get("input", {})
|
|
557
|
+
tool_desc = _format_tool_short(tool_name, tool_input)
|
|
558
|
+
dashboard.append_output(instance_idx, f"\u25b6 {tool_desc}")
|
|
559
|
+
|
|
560
|
+
elif msg_type == "content_block_delta":
|
|
561
|
+
delta = data.get("delta", {})
|
|
562
|
+
if delta.get("type") == "text_delta":
|
|
563
|
+
text = delta.get("text", "")
|
|
564
|
+
full_text.append(text)
|
|
565
|
+
# Only push meaningful chunks to dashboard
|
|
566
|
+
for text_line in text.split('\n'):
|
|
567
|
+
if text_line.strip():
|
|
568
|
+
dashboard.append_output(instance_idx, text_line)
|
|
569
|
+
|
|
570
|
+
elif msg_type == "tool_use":
|
|
571
|
+
tool_name = data.get("name", "unknown")
|
|
572
|
+
tool_input = data.get("input", {})
|
|
573
|
+
tool_desc = _format_tool_short(tool_name, tool_input)
|
|
574
|
+
dashboard.append_output(instance_idx, f"\u25b6 {tool_desc}")
|
|
575
|
+
|
|
576
|
+
elif msg_type == "tool_result":
|
|
577
|
+
dashboard.append_output(instance_idx, "\u2514\u2500 \u2713 done")
|
|
578
|
+
|
|
579
|
+
elif msg_type == "result":
|
|
580
|
+
text = data.get("result", "")
|
|
581
|
+
if text:
|
|
582
|
+
full_text.append(text)
|
|
583
|
+
|
|
584
|
+
elif msg_type == "error":
|
|
585
|
+
error_msg = data.get("error", {}).get("message", "Unknown error")
|
|
586
|
+
dashboard.append_output(instance_idx, f"\u2717 ERROR: {error_msg}")
|
|
587
|
+
|
|
588
|
+
except json.JSONDecodeError:
|
|
589
|
+
dashboard.append_output(instance_idx, line)
|
|
590
|
+
full_text.append(line)
|
|
591
|
+
|
|
592
|
+
process.wait(timeout=timeout)
|
|
593
|
+
return '\n'.join(full_text)
|
|
594
|
+
|
|
595
|
+
except subprocess.TimeoutExpired:
|
|
596
|
+
return f"[DAVELOOP:TIMEOUT] Claude Code timed out after {timeout // 60} minutes"
|
|
597
|
+
except FileNotFoundError:
|
|
598
|
+
return "[DAVELOOP:ERROR] Claude Code CLI not found"
|
|
599
|
+
except Exception as e:
|
|
600
|
+
return f"[DAVELOOP:ERROR] {str(e)}"
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _format_tool_short(tool_name: str, tool_input: dict) -> str:
|
|
604
|
+
"""Format a tool call for compact dashboard display."""
|
|
605
|
+
if tool_name == "Bash":
|
|
606
|
+
cmd = tool_input.get("command", "")
|
|
607
|
+
return f"Bash({cmd[:40]}{'...' if len(cmd) > 40 else ''})"
|
|
608
|
+
elif tool_name in ("Read", "Write", "Edit"):
|
|
609
|
+
fp = tool_input.get("file_path", "")
|
|
610
|
+
fname = fp.split("\\")[-1].split("/")[-1]
|
|
611
|
+
return f"{tool_name}({fname})"
|
|
612
|
+
elif tool_name == "Grep":
|
|
613
|
+
pat = tool_input.get("pattern", "")
|
|
614
|
+
return f"Grep({pat[:25]}{'...' if len(pat) > 25 else ''})"
|
|
615
|
+
elif tool_name == "Glob":
|
|
616
|
+
pat = tool_input.get("pattern", "")
|
|
617
|
+
return f"Glob({pat})"
|
|
618
|
+
elif tool_name == "Task":
|
|
619
|
+
desc = tool_input.get("description", "")
|
|
620
|
+
return f"Task({desc[:30]}{'...' if len(desc) > 30 else ''})"
|
|
621
|
+
return tool_name
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def run_turbo_task(task_desc: str, task_idx: int, system_prompt: str,
|
|
625
|
+
working_dir: str, max_iterations: int, timeout: int,
|
|
626
|
+
dashboard: TurboDashboard, swarm_mode: bool = False,
|
|
627
|
+
swarm_budget_max: int = 5, swarm_depth_max: int = 1) -> dict:
|
|
628
|
+
"""Run a single DaveLoop task inside the turbo dashboard.
|
|
629
|
+
|
|
630
|
+
Returns a dict with outcome, iterations, and output.
|
|
631
|
+
"""
|
|
632
|
+
dashboard.update_task(task_idx, task_desc[:50], max_iterations)
|
|
633
|
+
|
|
634
|
+
context = f"""
|
|
635
|
+
## Bug Report
|
|
636
|
+
|
|
637
|
+
{task_desc}
|
|
638
|
+
|
|
639
|
+
## Instructions
|
|
640
|
+
|
|
641
|
+
Analyze this bug. Gather whatever logs/information you need to understand it.
|
|
642
|
+
Then fix it. Use the reasoning protocol before each action.
|
|
643
|
+
"""
|
|
644
|
+
full_output = []
|
|
645
|
+
|
|
646
|
+
for iteration in range(1, max_iterations + 1):
|
|
647
|
+
dashboard.update_iteration(task_idx, iteration)
|
|
648
|
+
dashboard.append_output(task_idx, f"--- Iteration {iteration}/{max_iterations} ---")
|
|
649
|
+
|
|
650
|
+
if iteration == 1:
|
|
651
|
+
full_prompt = f"{system_prompt}\n\n---\n\n{context}"
|
|
652
|
+
else:
|
|
653
|
+
full_prompt = context
|
|
654
|
+
|
|
655
|
+
output = run_claude_code_turbo(
|
|
656
|
+
full_prompt, working_dir,
|
|
657
|
+
instance_idx=task_idx,
|
|
658
|
+
dashboard=dashboard,
|
|
659
|
+
timeout=timeout,
|
|
660
|
+
swarm_mode=swarm_mode,
|
|
661
|
+
swarm_budget_max=swarm_budget_max,
|
|
662
|
+
swarm_depth_max=swarm_depth_max,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
full_output.append(output)
|
|
666
|
+
|
|
667
|
+
signal, should_exit = check_exit_condition(output)
|
|
668
|
+
|
|
669
|
+
if should_exit:
|
|
670
|
+
if signal == "RESOLVED":
|
|
671
|
+
dashboard.append_output(task_idx, "\u2713 BUG RESOLVED!")
|
|
672
|
+
dashboard.mark_done(task_idx, "done")
|
|
673
|
+
return {"outcome": "RESOLVED", "iterations": iteration, "output": '\n'.join(full_output)}
|
|
674
|
+
elif signal == "BLOCKED":
|
|
675
|
+
dashboard.append_output(task_idx, "\u2717 BLOCKED - needs human help")
|
|
676
|
+
dashboard.mark_done(task_idx, "failed")
|
|
677
|
+
return {"outcome": "BLOCKED", "iterations": iteration, "output": '\n'.join(full_output)}
|
|
678
|
+
elif signal == "CLARIFY":
|
|
679
|
+
dashboard.append_output(task_idx, "\u2717 NEEDS CLARIFICATION")
|
|
680
|
+
dashboard.mark_done(task_idx, "failed")
|
|
681
|
+
return {"outcome": "CLARIFY", "iterations": iteration, "output": '\n'.join(full_output)}
|
|
682
|
+
else:
|
|
683
|
+
dashboard.append_output(task_idx, f"\u2717 Error: {signal}")
|
|
684
|
+
dashboard.mark_done(task_idx, "failed")
|
|
685
|
+
return {"outcome": signal, "iterations": iteration, "output": '\n'.join(full_output)}
|
|
686
|
+
|
|
687
|
+
# Prepare next iteration context
|
|
688
|
+
context = f"""
|
|
689
|
+
## Iteration {iteration + 1}
|
|
690
|
+
|
|
691
|
+
The bug is NOT yet resolved. You have full context from previous iterations.
|
|
692
|
+
|
|
693
|
+
Continue debugging. Analyze what happened, determine next steps, and proceed.
|
|
694
|
+
Use the reasoning protocol before each action.
|
|
695
|
+
"""
|
|
696
|
+
|
|
697
|
+
# Max iterations reached
|
|
698
|
+
dashboard.append_output(task_idx, f"\u2717 Max iterations ({max_iterations}) reached")
|
|
699
|
+
dashboard.mark_done(task_idx, "failed")
|
|
700
|
+
return {"outcome": "MAX_ITERATIONS", "iterations": max_iterations, "output": '\n'.join(full_output)}
|
|
701
|
+
|
|
702
|
+
|
|
321
703
|
# ============================================================================
|
|
322
704
|
# Token Tracker
|
|
323
705
|
# ============================================================================
|
|
@@ -1052,6 +1434,10 @@ def main():
|
|
|
1052
1434
|
help="Max sub-agent depth in swarm mode (default: 1, no recursive spawning)")
|
|
1053
1435
|
parser.add_argument("--show-tokens", action="store_true",
|
|
1054
1436
|
help="Show verbose per-turn token usage during execution")
|
|
1437
|
+
parser.add_argument("--turbo", action="store_true",
|
|
1438
|
+
help="Run all tasks in parallel with a Rich split-pane dashboard")
|
|
1439
|
+
parser.add_argument("--turbo-workers", type=int, default=None,
|
|
1440
|
+
help="Max parallel workers in turbo mode (default: number of tasks)")
|
|
1055
1441
|
|
|
1056
1442
|
args = parser.parse_args()
|
|
1057
1443
|
|
|
@@ -1106,6 +1492,9 @@ def main():
|
|
|
1106
1492
|
print_status("Tools", ALLOWED_TOOLS_SWARM, C.WHITE)
|
|
1107
1493
|
else:
|
|
1108
1494
|
print_status("Tools", ALLOWED_TOOLS_DEFAULT, C.WHITE)
|
|
1495
|
+
if args.turbo:
|
|
1496
|
+
workers = args.turbo_workers or len(bug_descriptions)
|
|
1497
|
+
print_status("Turbo", f"ENABLED ({workers} parallel workers)", C.BRIGHT_MAGENTA)
|
|
1109
1498
|
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
1110
1499
|
|
|
1111
1500
|
# Build task queue
|
|
@@ -1118,15 +1507,97 @@ def main():
|
|
|
1118
1507
|
print(f"{C.BRIGHT_BLUE}│{C.RESET} Type while running: {C.BRIGHT_WHITE}wait{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}pause{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}add{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}done{C.RESET} {C.DIM}·{C.RESET} {C.BRIGHT_WHITE}stop{C.RESET} {C.BRIGHT_BLUE}│{C.RESET}")
|
|
1119
1508
|
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
1120
1509
|
|
|
1121
|
-
# Start input monitor
|
|
1122
|
-
input_monitor = InputMonitor()
|
|
1123
|
-
input_monitor.start()
|
|
1124
|
-
|
|
1125
1510
|
# Build history context for initial prompt
|
|
1126
1511
|
history_context = ""
|
|
1127
1512
|
if history_data["sessions"]:
|
|
1128
1513
|
history_context = "\n\n" + format_history_context(history_data["sessions"])
|
|
1129
1514
|
|
|
1515
|
+
# ========================================================================
|
|
1516
|
+
# TURBO MODE: parallel execution with Rich dashboard
|
|
1517
|
+
# ========================================================================
|
|
1518
|
+
if args.turbo and len(bug_descriptions) >= 1:
|
|
1519
|
+
num_workers = args.turbo_workers or len(bug_descriptions)
|
|
1520
|
+
num_workers = min(num_workers, len(bug_descriptions))
|
|
1521
|
+
|
|
1522
|
+
print(f"\n {C.BRIGHT_MAGENTA}{C.BOLD}◆ Launching Turbo Mode with {num_workers} parallel workers...{C.RESET}\n")
|
|
1523
|
+
time.sleep(1) # Brief pause so user sees the message before dashboard takes over
|
|
1524
|
+
|
|
1525
|
+
dashboard = TurboDashboard(len(bug_descriptions))
|
|
1526
|
+
system_prompt_full = system_prompt + history_context
|
|
1527
|
+
|
|
1528
|
+
# Launch all tasks in parallel
|
|
1529
|
+
results = [None] * len(bug_descriptions)
|
|
1530
|
+
|
|
1531
|
+
dashboard.start()
|
|
1532
|
+
try:
|
|
1533
|
+
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
1534
|
+
futures = {}
|
|
1535
|
+
for idx, desc in enumerate(bug_descriptions):
|
|
1536
|
+
future = executor.submit(
|
|
1537
|
+
run_turbo_task,
|
|
1538
|
+
task_desc=desc,
|
|
1539
|
+
task_idx=idx,
|
|
1540
|
+
system_prompt=system_prompt_full,
|
|
1541
|
+
working_dir=working_dir,
|
|
1542
|
+
max_iterations=args.max_iterations,
|
|
1543
|
+
timeout=args.timeout,
|
|
1544
|
+
dashboard=dashboard,
|
|
1545
|
+
swarm_mode=args.swarm,
|
|
1546
|
+
swarm_budget_max=args.swarm_budget,
|
|
1547
|
+
swarm_depth_max=args.swarm_depth,
|
|
1548
|
+
)
|
|
1549
|
+
futures[future] = idx
|
|
1550
|
+
|
|
1551
|
+
for future in as_completed(futures):
|
|
1552
|
+
idx = futures[future]
|
|
1553
|
+
try:
|
|
1554
|
+
results[idx] = future.result()
|
|
1555
|
+
except Exception as e:
|
|
1556
|
+
results[idx] = {"outcome": "ERROR", "iterations": 0, "output": str(e)}
|
|
1557
|
+
dashboard.append_output(idx, f"\u2717 Exception: {e}")
|
|
1558
|
+
dashboard.mark_done(idx, "failed")
|
|
1559
|
+
finally:
|
|
1560
|
+
# Keep dashboard visible for a moment so user can see final state
|
|
1561
|
+
time.sleep(2)
|
|
1562
|
+
dashboard.stop()
|
|
1563
|
+
|
|
1564
|
+
# Print final summary
|
|
1565
|
+
print(f"\n{BANNER}\n")
|
|
1566
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ TURBO MODE COMPLETE{C.RESET}")
|
|
1567
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
1568
|
+
for idx, desc in enumerate(bug_descriptions):
|
|
1569
|
+
r = results[idx]
|
|
1570
|
+
outcome = r["outcome"] if r else "ERROR"
|
|
1571
|
+
iters = r["iterations"] if r else 0
|
|
1572
|
+
desc_short = desc[:55]
|
|
1573
|
+
if outcome == "RESOLVED":
|
|
1574
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{desc_short}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
1575
|
+
else:
|
|
1576
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.RED}{desc_short}{C.RESET} {C.DIM}({outcome}, {iters} iter){C.RESET}")
|
|
1577
|
+
|
|
1578
|
+
# Save to history
|
|
1579
|
+
session_entry = summarize_session(desc, outcome, iters)
|
|
1580
|
+
history_data["sessions"].append(session_entry)
|
|
1581
|
+
|
|
1582
|
+
# Save log
|
|
1583
|
+
save_log(idx + 1, r.get("output", "") if r else "", session_id)
|
|
1584
|
+
|
|
1585
|
+
save_history(working_dir, history_data)
|
|
1586
|
+
|
|
1587
|
+
print(f"\n {C.DIM}Session: {session_id}{C.RESET}")
|
|
1588
|
+
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
1589
|
+
|
|
1590
|
+
all_resolved = all(r and r["outcome"] == "RESOLVED" for r in results)
|
|
1591
|
+
return 0 if all_resolved else 1
|
|
1592
|
+
|
|
1593
|
+
# ========================================================================
|
|
1594
|
+
# SEQUENTIAL MODE (original behavior)
|
|
1595
|
+
# ========================================================================
|
|
1596
|
+
|
|
1597
|
+
# Start input monitor
|
|
1598
|
+
input_monitor = InputMonitor()
|
|
1599
|
+
input_monitor.start()
|
|
1600
|
+
|
|
1130
1601
|
# Session-wide token tracking (aggregates across all tasks)
|
|
1131
1602
|
session_token_tracker = TokenTracker()
|
|
1132
1603
|
|
|
@@ -13,7 +13,7 @@ long_description = readme_file.read_text(encoding="utf-8") if readme_file.exists
|
|
|
13
13
|
|
|
14
14
|
setup(
|
|
15
15
|
name="daveloop",
|
|
16
|
-
version="1.5.
|
|
16
|
+
version="1.5.1",
|
|
17
17
|
description="Self-healing debug agent powered by Claude Code CLI",
|
|
18
18
|
long_description=long_description,
|
|
19
19
|
long_description_content_type="text/markdown",
|
|
@@ -22,8 +22,11 @@ setup(
|
|
|
22
22
|
py_modules=["daveloop", "daveloop_swebench"],
|
|
23
23
|
python_requires=">=3.7",
|
|
24
24
|
install_requires=[
|
|
25
|
-
# No external dependencies - uses only stdlib
|
|
25
|
+
# No external dependencies for core - uses only stdlib
|
|
26
26
|
],
|
|
27
|
+
extras_require={
|
|
28
|
+
"turbo": ["rich>=13.0"],
|
|
29
|
+
},
|
|
27
30
|
entry_points={
|
|
28
31
|
"console_scripts": [
|
|
29
32
|
"daveloop=daveloop:main",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|