daveloop 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
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.2.0.dist-info → daveloop-1.3.0.dist-info}/METADATA +10 -2
- daveloop-1.3.0.dist-info/RECORD +7 -0
- {daveloop-1.2.0.dist-info → daveloop-1.3.0.dist-info}/WHEEL +1 -1
- daveloop.py +434 -84
- daveloop-1.2.0.dist-info/RECORD +0 -7
- {daveloop-1.2.0.dist-info → daveloop-1.3.0.dist-info}/entry_points.txt +0 -0
- {daveloop-1.2.0.dist-info → daveloop-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: daveloop
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
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,14 @@ 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
|
+
Dynamic: author
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: keywords
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
22
30
|
|
|
23
31
|
# DaveLoop
|
|
24
32
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
daveloop.py,sha256=Htq9-ga4EPeyfIUW2Llq6yz7TJi4umfWD7-zlQPGZLs,49406
|
|
2
|
+
daveloop_swebench.py,sha256=iD9AU3XRiMQpt7TknFNlvnmPCNp64V-JaTfqTFgsGBM,15996
|
|
3
|
+
daveloop-1.3.0.dist-info/METADATA,sha256=eOrj3xTjeHWdrS9_Y87_H3KfHYa3KOtv5fXBDDC485w,10463
|
|
4
|
+
daveloop-1.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
daveloop-1.3.0.dist-info/entry_points.txt,sha256=QcFAZgFrDfPtIikNQb7eW9DxOpBK7T-qWrKqbGAS9Ww,86
|
|
6
|
+
daveloop-1.3.0.dist-info/top_level.txt,sha256=36DiYt70m4DIK8t7IhV_y6hAzUIyeb5-qDUf3-gbDdg,27
|
|
7
|
+
daveloop-1.3.0.dist-info/RECORD,,
|
daveloop.py
CHANGED
|
@@ -11,6 +11,7 @@ import argparse
|
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
13
|
import itertools
|
|
14
|
+
import json
|
|
14
15
|
from datetime import datetime
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
@@ -195,6 +196,144 @@ class Spinner:
|
|
|
195
196
|
sys.stdout.write(f"\r {C.GREEN}✓{C.RESET} {self.message} complete {C.DIM}({elapsed:.1f}s){C.RESET} \n")
|
|
196
197
|
sys.stdout.flush()
|
|
197
198
|
|
|
199
|
+
# ============================================================================
|
|
200
|
+
# Task Queue
|
|
201
|
+
# ============================================================================
|
|
202
|
+
class TaskQueue:
|
|
203
|
+
"""Manages multiple bug tasks in sequence."""
|
|
204
|
+
|
|
205
|
+
def __init__(self):
|
|
206
|
+
self.tasks = [] # list of {"description": str, "status": "pending"|"active"|"done"|"failed"}
|
|
207
|
+
|
|
208
|
+
def add(self, description: str):
|
|
209
|
+
"""Add a new task with pending status."""
|
|
210
|
+
self.tasks.append({"description": description, "status": "pending"})
|
|
211
|
+
|
|
212
|
+
def next(self):
|
|
213
|
+
"""Find first pending task, set it to active, return it. None if no pending tasks."""
|
|
214
|
+
for task in self.tasks:
|
|
215
|
+
if task["status"] == "pending":
|
|
216
|
+
task["status"] = "active"
|
|
217
|
+
return task
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
def current(self):
|
|
221
|
+
"""Return the task with status active, or None."""
|
|
222
|
+
for task in self.tasks:
|
|
223
|
+
if task["status"] == "active":
|
|
224
|
+
return task
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def mark_done(self):
|
|
228
|
+
"""Set current active task to done."""
|
|
229
|
+
task = self.current()
|
|
230
|
+
if task:
|
|
231
|
+
task["status"] = "done"
|
|
232
|
+
|
|
233
|
+
def mark_failed(self):
|
|
234
|
+
"""Set current active task to failed."""
|
|
235
|
+
task = self.current()
|
|
236
|
+
if task:
|
|
237
|
+
task["status"] = "failed"
|
|
238
|
+
|
|
239
|
+
def remaining(self) -> int:
|
|
240
|
+
"""Count of pending tasks."""
|
|
241
|
+
return sum(1 for t in self.tasks if t["status"] == "pending")
|
|
242
|
+
|
|
243
|
+
def all(self):
|
|
244
|
+
"""Return all tasks."""
|
|
245
|
+
return self.tasks
|
|
246
|
+
|
|
247
|
+
def summary_display(self):
|
|
248
|
+
"""Print a nice box showing all tasks with status icons."""
|
|
249
|
+
active_count = sum(1 for t in self.tasks if t["status"] == "active")
|
|
250
|
+
done_count = sum(1 for t in self.tasks if t["status"] == "done")
|
|
251
|
+
total = len(self.tasks)
|
|
252
|
+
active_idx = next((i for i, t in enumerate(self.tasks) if t["status"] == "active"), 0)
|
|
253
|
+
|
|
254
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ TASK QUEUE ({active_idx + 1}/{total} active){C.RESET}")
|
|
255
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
256
|
+
for task in self.tasks:
|
|
257
|
+
desc = task["description"][:50]
|
|
258
|
+
if task["status"] == "done":
|
|
259
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{desc}{C.RESET}")
|
|
260
|
+
elif task["status"] == "active":
|
|
261
|
+
print(f" {C.BRIGHT_CYAN}▶{C.RESET} {C.BRIGHT_WHITE}{desc}{C.RESET} {C.DIM}(active){C.RESET}")
|
|
262
|
+
elif task["status"] == "pending":
|
|
263
|
+
print(f" {C.DIM}○{C.RESET} {C.DIM}{desc}{C.RESET} {C.DIM}(pending){C.RESET}")
|
|
264
|
+
elif task["status"] == "failed":
|
|
265
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.RED}{desc}{C.RESET}")
|
|
266
|
+
print()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ============================================================================
|
|
270
|
+
# Session Memory
|
|
271
|
+
# ============================================================================
|
|
272
|
+
def load_history(working_dir: str) -> dict:
|
|
273
|
+
"""Read .daveloop_history.json from working_dir. Return default if missing or corrupted."""
|
|
274
|
+
history_file = Path(working_dir) / ".daveloop_history.json"
|
|
275
|
+
if not history_file.exists():
|
|
276
|
+
return {"sessions": []}
|
|
277
|
+
try:
|
|
278
|
+
data = json.loads(history_file.read_text(encoding="utf-8"))
|
|
279
|
+
if not isinstance(data, dict) or "sessions" not in data:
|
|
280
|
+
print_warning_box("Corrupted history file - resetting")
|
|
281
|
+
return {"sessions": []}
|
|
282
|
+
return data
|
|
283
|
+
except (json.JSONDecodeError, ValueError):
|
|
284
|
+
print_warning_box("Corrupted history JSON - resetting")
|
|
285
|
+
return {"sessions": []}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def save_history(working_dir: str, history_data: dict):
|
|
289
|
+
"""Write to .daveloop_history.json. Keep only last 20 sessions."""
|
|
290
|
+
history_file = Path(working_dir) / ".daveloop_history.json"
|
|
291
|
+
history_data["sessions"] = history_data["sessions"][-20:]
|
|
292
|
+
history_file.write_text(json.dumps(history_data, indent=2), encoding="utf-8")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def summarize_session(bug: str, outcome: str, iterations: int) -> dict:
|
|
296
|
+
"""Return a dict summarizing a session."""
|
|
297
|
+
now = datetime.now()
|
|
298
|
+
return {
|
|
299
|
+
"session_id": now.strftime("%Y%m%d_%H%M%S"),
|
|
300
|
+
"bug": bug,
|
|
301
|
+
"outcome": outcome,
|
|
302
|
+
"iterations": iterations,
|
|
303
|
+
"timestamp": now.isoformat()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def format_history_context(sessions: list) -> str:
|
|
308
|
+
"""Return markdown string summarizing recent sessions for Claude context."""
|
|
309
|
+
if not sessions:
|
|
310
|
+
return ""
|
|
311
|
+
lines = ["## Previous DaveLoop Sessions"]
|
|
312
|
+
for s in sessions[-10:]: # Show last 10
|
|
313
|
+
outcome = s.get("outcome", "UNKNOWN")
|
|
314
|
+
bug = s.get("bug", "unknown")[:60]
|
|
315
|
+
iters = s.get("iterations", "?")
|
|
316
|
+
lines.append(f"- [{outcome}] \"{bug}\" ({iters} iterations)")
|
|
317
|
+
return "\n".join(lines)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def print_history_box(sessions: list):
|
|
321
|
+
"""Print a nice UI box showing loaded history."""
|
|
322
|
+
if not sessions:
|
|
323
|
+
return
|
|
324
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ SESSION HISTORY{C.RESET}")
|
|
325
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
326
|
+
for s in sessions[-10:]:
|
|
327
|
+
outcome = s.get("outcome", "UNKNOWN")
|
|
328
|
+
bug = s.get("bug", "unknown")[:55]
|
|
329
|
+
iters = s.get("iterations", "?")
|
|
330
|
+
if outcome == "RESOLVED":
|
|
331
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
332
|
+
else:
|
|
333
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
334
|
+
print()
|
|
335
|
+
|
|
336
|
+
|
|
198
337
|
# ============================================================================
|
|
199
338
|
# Output Formatter
|
|
200
339
|
# ============================================================================
|
|
@@ -274,6 +413,74 @@ def format_claude_output(output: str) -> str:
|
|
|
274
413
|
|
|
275
414
|
return '\n'.join(formatted)
|
|
276
415
|
|
|
416
|
+
# ============================================================================
|
|
417
|
+
# Input Monitor
|
|
418
|
+
# ============================================================================
|
|
419
|
+
class InputMonitor:
|
|
420
|
+
"""Daemon thread that reads stdin for commands while Claude is running.
|
|
421
|
+
|
|
422
|
+
After detecting a command, stops reading stdin so that input() calls
|
|
423
|
+
in the main thread can safely read without a race condition.
|
|
424
|
+
Call resume_reading() after the main thread is done with input().
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
VALID_COMMANDS = ("wait", "pause", "add", "done")
|
|
428
|
+
|
|
429
|
+
def __init__(self):
|
|
430
|
+
self._command = None
|
|
431
|
+
self._lock = threading.Lock()
|
|
432
|
+
self._thread = threading.Thread(target=self._read_loop, daemon=True)
|
|
433
|
+
self._running = False
|
|
434
|
+
self._read_gate = threading.Event()
|
|
435
|
+
self._read_gate.set() # Start with reading enabled
|
|
436
|
+
|
|
437
|
+
def start(self):
|
|
438
|
+
"""Start monitoring stdin."""
|
|
439
|
+
self._running = True
|
|
440
|
+
self._thread.start()
|
|
441
|
+
|
|
442
|
+
def stop(self):
|
|
443
|
+
"""Stop monitoring stdin."""
|
|
444
|
+
self._running = False
|
|
445
|
+
self._read_gate.set() # Unblock the thread so it can exit
|
|
446
|
+
|
|
447
|
+
def resume_reading(self):
|
|
448
|
+
"""Resume reading stdin after an interrupt has been handled."""
|
|
449
|
+
self._read_gate.set()
|
|
450
|
+
|
|
451
|
+
def _read_loop(self):
|
|
452
|
+
"""Read lines from stdin, looking for valid commands."""
|
|
453
|
+
while self._running:
|
|
454
|
+
# Wait until reading is enabled (blocks after a command is detected)
|
|
455
|
+
self._read_gate.wait()
|
|
456
|
+
if not self._running:
|
|
457
|
+
break
|
|
458
|
+
try:
|
|
459
|
+
line = sys.stdin.readline()
|
|
460
|
+
if not line:
|
|
461
|
+
break
|
|
462
|
+
cmd = line.strip().lower()
|
|
463
|
+
if cmd in self.VALID_COMMANDS:
|
|
464
|
+
with self._lock:
|
|
465
|
+
self._command = cmd
|
|
466
|
+
# Stop reading so input() in the main thread has no competition
|
|
467
|
+
self._read_gate.clear()
|
|
468
|
+
except (EOFError, OSError):
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
def has_command(self) -> bool:
|
|
472
|
+
"""Check if a command has been received."""
|
|
473
|
+
with self._lock:
|
|
474
|
+
return self._command is not None
|
|
475
|
+
|
|
476
|
+
def get_command(self) -> str:
|
|
477
|
+
"""Get and clear the current command."""
|
|
478
|
+
with self._lock:
|
|
479
|
+
cmd = self._command
|
|
480
|
+
self._command = None
|
|
481
|
+
return cmd
|
|
482
|
+
|
|
483
|
+
|
|
277
484
|
# ============================================================================
|
|
278
485
|
# Core Functions
|
|
279
486
|
# ============================================================================
|
|
@@ -327,11 +534,12 @@ def find_claude_cli():
|
|
|
327
534
|
return None
|
|
328
535
|
|
|
329
536
|
|
|
330
|
-
def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool = False, stream: bool = True, timeout: int = DEFAULT_TIMEOUT) -> str:
|
|
537
|
+
def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool = False, stream: bool = True, timeout: int = DEFAULT_TIMEOUT, input_monitor=None) -> str:
|
|
331
538
|
"""Execute Claude Code CLI with the given prompt.
|
|
332
539
|
|
|
333
540
|
If stream=True, output is printed in real-time and also returned.
|
|
334
541
|
timeout is in seconds (default 600 = 10 minutes).
|
|
542
|
+
input_monitor: optional InputMonitor to check for user commands during execution.
|
|
335
543
|
"""
|
|
336
544
|
claude_cmd = find_claude_cli()
|
|
337
545
|
if not claude_cmd:
|
|
@@ -375,7 +583,6 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
375
583
|
start_time = time.time()
|
|
376
584
|
|
|
377
585
|
# Read and display JSON stream output
|
|
378
|
-
import json
|
|
379
586
|
output_lines = []
|
|
380
587
|
full_text = []
|
|
381
588
|
|
|
@@ -497,12 +704,9 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
497
704
|
print(f" {C.BRIGHT_BLUE}└─{C.RESET} {C.GREEN}✓{C.RESET}")
|
|
498
705
|
|
|
499
706
|
elif msg_type == "result":
|
|
500
|
-
# Final result
|
|
707
|
+
# Final result - skip printing as it duplicates streamed content
|
|
501
708
|
text = data.get("result", "")
|
|
502
709
|
if text:
|
|
503
|
-
for line_text in text.split('\n'):
|
|
504
|
-
formatted = format_output_line(line_text)
|
|
505
|
-
print(formatted)
|
|
506
710
|
full_text.append(text)
|
|
507
711
|
|
|
508
712
|
elif msg_type == "error":
|
|
@@ -518,6 +722,16 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
518
722
|
|
|
519
723
|
output_lines.append(line)
|
|
520
724
|
|
|
725
|
+
# Check for user commands from InputMonitor
|
|
726
|
+
if input_monitor and input_monitor.has_command():
|
|
727
|
+
user_cmd = input_monitor.get_command()
|
|
728
|
+
try:
|
|
729
|
+
process.terminate()
|
|
730
|
+
process.wait(timeout=5)
|
|
731
|
+
except Exception:
|
|
732
|
+
pass
|
|
733
|
+
return f"[DAVELOOP:INTERRUPTED:{user_cmd}]"
|
|
734
|
+
|
|
521
735
|
process.wait(timeout=timeout)
|
|
522
736
|
return '\n'.join(full_text)
|
|
523
737
|
else:
|
|
@@ -616,7 +830,7 @@ def main():
|
|
|
616
830
|
description="DaveLoop - Self-Healing Debug Agent",
|
|
617
831
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
618
832
|
)
|
|
619
|
-
parser.add_argument("bug", nargs="
|
|
833
|
+
parser.add_argument("bug", nargs="*", help="Bug description(s) or error message(s)")
|
|
620
834
|
parser.add_argument("-f", "--file", help="Read bug description from file")
|
|
621
835
|
parser.add_argument("-d", "--dir", help="Working directory for Claude Code")
|
|
622
836
|
parser.add_argument("-m", "--max-iterations", type=int, default=MAX_ITERATIONS)
|
|
@@ -630,16 +844,19 @@ def main():
|
|
|
630
844
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
631
845
|
print(BANNER)
|
|
632
846
|
|
|
633
|
-
#
|
|
847
|
+
# Collect bug descriptions
|
|
848
|
+
bug_descriptions = []
|
|
634
849
|
if args.file:
|
|
635
|
-
|
|
850
|
+
bug_descriptions.append(Path(args.file).read_text(encoding="utf-8"))
|
|
636
851
|
elif args.bug:
|
|
637
|
-
|
|
852
|
+
bug_descriptions.extend(args.bug)
|
|
638
853
|
else:
|
|
639
854
|
print(f" {C.CYAN}Describe the bug (Ctrl+D or Ctrl+Z to finish):{C.RESET}")
|
|
640
|
-
|
|
855
|
+
stdin_input = sys.stdin.read().strip()
|
|
856
|
+
if stdin_input:
|
|
857
|
+
bug_descriptions.append(stdin_input)
|
|
641
858
|
|
|
642
|
-
if not
|
|
859
|
+
if not bug_descriptions:
|
|
643
860
|
print_error_box("No bug description provided")
|
|
644
861
|
return 1
|
|
645
862
|
|
|
@@ -648,28 +865,61 @@ def main():
|
|
|
648
865
|
system_prompt = load_prompt()
|
|
649
866
|
working_dir = args.dir or os.getcwd()
|
|
650
867
|
|
|
868
|
+
# Load session history
|
|
869
|
+
history_data = load_history(working_dir)
|
|
870
|
+
if history_data["sessions"]:
|
|
871
|
+
print_history_box(history_data["sessions"])
|
|
872
|
+
|
|
651
873
|
# Session info
|
|
652
874
|
print_header_box(f"SESSION: {session_id}", C.BRIGHT_BLUE)
|
|
653
875
|
print_status("Directory", working_dir, C.WHITE)
|
|
654
876
|
print_status("Iterations", str(args.max_iterations), C.WHITE)
|
|
655
877
|
print_status("Timeout", f"{args.timeout // 60}m per iteration", C.WHITE)
|
|
878
|
+
print_status("Tasks", str(len(bug_descriptions)), C.WHITE)
|
|
656
879
|
print_status("Mode", "Autonomous", C.WHITE)
|
|
657
880
|
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
658
881
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
for
|
|
662
|
-
|
|
663
|
-
if len(bug_input.split('\n')) > 8:
|
|
664
|
-
print(f" {C.RED}... +{len(bug_input.split(chr(10))) - 8} more lines{C.RESET}")
|
|
882
|
+
# Build task queue
|
|
883
|
+
task_queue = TaskQueue()
|
|
884
|
+
for desc in bug_descriptions:
|
|
885
|
+
task_queue.add(desc)
|
|
665
886
|
|
|
666
|
-
|
|
887
|
+
# Print controls hint
|
|
888
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}┌─ CONTROLS {'─' * 58}┐{C.RESET}")
|
|
889
|
+
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.BRIGHT_BLUE}│{C.RESET}")
|
|
890
|
+
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
667
891
|
|
|
668
|
-
#
|
|
669
|
-
|
|
892
|
+
# Start input monitor
|
|
893
|
+
input_monitor = InputMonitor()
|
|
894
|
+
input_monitor.start()
|
|
895
|
+
|
|
896
|
+
# Build history context for initial prompt
|
|
897
|
+
history_context = ""
|
|
898
|
+
if history_data["sessions"]:
|
|
899
|
+
history_context = "\n\n" + format_history_context(history_data["sessions"])
|
|
900
|
+
|
|
901
|
+
# === OUTER LOOP: iterate over tasks ===
|
|
902
|
+
while True:
|
|
903
|
+
task = task_queue.next()
|
|
904
|
+
if task is None:
|
|
905
|
+
break
|
|
906
|
+
|
|
907
|
+
bug_input = task["description"]
|
|
908
|
+
task_queue.summary_display()
|
|
909
|
+
|
|
910
|
+
print_section("BUG REPORT", C.BRIGHT_RED)
|
|
911
|
+
for line in bug_input.split('\n')[:8]:
|
|
912
|
+
print(f" {C.BRIGHT_RED}{line[:70]}{C.RESET}")
|
|
913
|
+
if len(bug_input.split('\n')) > 8:
|
|
914
|
+
print(f" {C.RED}... +{len(bug_input.split(chr(10))) - 8} more lines{C.RESET}")
|
|
915
|
+
sys.stdout.flush()
|
|
916
|
+
|
|
917
|
+
# Initial context for this task
|
|
918
|
+
context = f"""
|
|
670
919
|
## Bug Report
|
|
671
920
|
|
|
672
921
|
{bug_input}
|
|
922
|
+
{history_context}
|
|
673
923
|
|
|
674
924
|
## Instructions
|
|
675
925
|
|
|
@@ -677,66 +927,143 @@ Analyze this bug. Gather whatever logs/information you need to understand it.
|
|
|
677
927
|
Then fix it. Use the reasoning protocol before each action.
|
|
678
928
|
"""
|
|
679
929
|
|
|
680
|
-
|
|
930
|
+
iteration_history = []
|
|
681
931
|
|
|
682
|
-
|
|
932
|
+
# === INNER LOOP: iterations for current task ===
|
|
933
|
+
for iteration in range(1, args.max_iterations + 1):
|
|
683
934
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
935
|
+
if iteration == 1:
|
|
936
|
+
full_prompt = f"{system_prompt}\n\n---\n\n{context}"
|
|
937
|
+
continue_session = False
|
|
938
|
+
else:
|
|
939
|
+
full_prompt = context
|
|
940
|
+
continue_session = True
|
|
690
941
|
|
|
691
|
-
|
|
692
|
-
|
|
942
|
+
if args.verbose:
|
|
943
|
+
print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
|
|
693
944
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
945
|
+
# Show "Claude is working" indicator
|
|
946
|
+
print(f"\n {C.BRIGHT_BLUE}◆ Agent active...{C.RESET}\n")
|
|
947
|
+
sys.stdout.flush()
|
|
697
948
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
949
|
+
# Run Claude with real-time streaming output
|
|
950
|
+
output = run_claude_code(
|
|
951
|
+
full_prompt, working_dir,
|
|
952
|
+
continue_session=continue_session,
|
|
953
|
+
stream=True, timeout=args.timeout,
|
|
954
|
+
input_monitor=input_monitor
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
print(f"\n{C.BRIGHT_BLUE} {'─' * 70}{C.RESET}")
|
|
958
|
+
|
|
959
|
+
# Save log
|
|
960
|
+
save_log(iteration, output, session_id)
|
|
961
|
+
iteration_history.append(output)
|
|
962
|
+
|
|
963
|
+
# Check for user interrupt commands
|
|
964
|
+
if "[DAVELOOP:INTERRUPTED:" in output:
|
|
965
|
+
# Extract the command name
|
|
966
|
+
cmd_start = output.index("[DAVELOOP:INTERRUPTED:") + len("[DAVELOOP:INTERRUPTED:")
|
|
967
|
+
cmd_end = output.index("]", cmd_start)
|
|
968
|
+
user_cmd = output[cmd_start:cmd_end]
|
|
969
|
+
|
|
970
|
+
if user_cmd in ("wait", "pause"):
|
|
971
|
+
# Pause and get user correction
|
|
972
|
+
print(f"\n{C.BRIGHT_YELLOW}{C.BOLD} \u23f8 PAUSED - DaveLoop is waiting for your input{C.RESET}")
|
|
973
|
+
print(f"{C.BRIGHT_YELLOW} {'─' * 70}{C.RESET}")
|
|
974
|
+
print(f" {C.WHITE} Type your correction or additional context:{C.RESET}")
|
|
975
|
+
try:
|
|
976
|
+
human_input = input(f" {C.WHITE}> {C.RESET}")
|
|
977
|
+
except EOFError:
|
|
978
|
+
human_input = ""
|
|
979
|
+
input_monitor.resume_reading()
|
|
980
|
+
context = f"""
|
|
981
|
+
## Human Correction (pause/wait command)
|
|
982
|
+
|
|
983
|
+
{human_input}
|
|
984
|
+
|
|
985
|
+
Continue debugging with this corrected context. Use the reasoning protocol before each action.
|
|
986
|
+
"""
|
|
987
|
+
continue
|
|
988
|
+
|
|
989
|
+
elif user_cmd == "add":
|
|
990
|
+
# Prompt for new task, then resume current
|
|
991
|
+
print(f"\n {C.BRIGHT_CYAN}Enter new task description:{C.RESET}")
|
|
992
|
+
try:
|
|
993
|
+
new_desc = input(f" {C.WHITE}> {C.RESET}")
|
|
994
|
+
except EOFError:
|
|
995
|
+
new_desc = ""
|
|
996
|
+
input_monitor.resume_reading()
|
|
997
|
+
if new_desc.strip():
|
|
998
|
+
task_queue.add(new_desc.strip())
|
|
999
|
+
print(f" {C.GREEN}✓{C.RESET} Task added to queue")
|
|
1000
|
+
task_queue.summary_display()
|
|
1001
|
+
# Resume current task with --continue
|
|
1002
|
+
context = f"""
|
|
1003
|
+
## Continuing after user added a new task to the queue
|
|
1004
|
+
|
|
1005
|
+
Continue the current debugging task. Use the reasoning protocol before each action.
|
|
1006
|
+
"""
|
|
1007
|
+
continue
|
|
1008
|
+
|
|
1009
|
+
elif user_cmd == "done":
|
|
1010
|
+
# Clean exit
|
|
1011
|
+
input_monitor.stop()
|
|
1012
|
+
session_entry = summarize_session(bug_input, "DONE_BY_USER", iteration)
|
|
1013
|
+
history_data["sessions"].append(session_entry)
|
|
1014
|
+
save_history(working_dir, history_data)
|
|
1015
|
+
print(f"\n {C.GREEN}✓{C.RESET} Session saved. Exiting by user request.")
|
|
1016
|
+
return 0
|
|
1017
|
+
|
|
1018
|
+
# Check exit condition
|
|
1019
|
+
signal, should_exit = check_exit_condition(output)
|
|
1020
|
+
|
|
1021
|
+
if should_exit:
|
|
1022
|
+
if signal == "RESOLVED":
|
|
1023
|
+
print_success_box("")
|
|
1024
|
+
print(f" {C.DIM}Session: {session_id}{C.RESET}")
|
|
1025
|
+
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
1026
|
+
task_queue.mark_done()
|
|
1027
|
+
session_entry = summarize_session(bug_input, "RESOLVED", iteration)
|
|
1028
|
+
history_data["sessions"].append(session_entry)
|
|
1029
|
+
save_history(working_dir, history_data)
|
|
1030
|
+
break # Move to next task
|
|
1031
|
+
elif signal == "CLARIFY":
|
|
1032
|
+
print_warning_box("Claude needs clarification")
|
|
1033
|
+
print(f"\n {C.BLUE}Your response:{C.RESET}")
|
|
1034
|
+
try:
|
|
1035
|
+
human_input = input(f" {C.WHITE}> {C.RESET}")
|
|
1036
|
+
except EOFError:
|
|
1037
|
+
human_input = ""
|
|
1038
|
+
input_monitor.resume_reading()
|
|
1039
|
+
context = f"""
|
|
721
1040
|
## Human Clarification
|
|
722
1041
|
|
|
723
1042
|
{human_input}
|
|
724
1043
|
|
|
725
1044
|
Continue debugging with this information. Use the reasoning protocol before each action.
|
|
726
1045
|
"""
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1046
|
+
continue
|
|
1047
|
+
elif signal == "BLOCKED":
|
|
1048
|
+
print_error_box("Claude is blocked - needs human help")
|
|
1049
|
+
print_status("Session", session_id, C.WHITE)
|
|
1050
|
+
print_status("Logs", str(LOG_DIR), C.WHITE)
|
|
1051
|
+
print()
|
|
1052
|
+
task_queue.mark_failed()
|
|
1053
|
+
session_entry = summarize_session(bug_input, "BLOCKED", iteration)
|
|
1054
|
+
history_data["sessions"].append(session_entry)
|
|
1055
|
+
save_history(working_dir, history_data)
|
|
1056
|
+
break # Move to next task
|
|
1057
|
+
else:
|
|
1058
|
+
print_error_box(f"Error occurred: {signal}")
|
|
1059
|
+
task_queue.mark_failed()
|
|
1060
|
+
session_entry = summarize_session(bug_input, "ERROR", iteration)
|
|
1061
|
+
history_data["sessions"].append(session_entry)
|
|
1062
|
+
save_history(working_dir, history_data)
|
|
1063
|
+
break # Move to next task
|
|
1064
|
+
|
|
1065
|
+
# Prepare context for next iteration
|
|
1066
|
+
context = f"""
|
|
740
1067
|
## Iteration {iteration + 1}
|
|
741
1068
|
|
|
742
1069
|
The bug is NOT yet resolved. You have full context from previous iterations.
|
|
@@ -744,23 +1071,46 @@ The bug is NOT yet resolved. You have full context from previous iterations.
|
|
|
744
1071
|
Continue debugging. Analyze what happened, determine next steps, and proceed.
|
|
745
1072
|
Use the reasoning protocol before each action.
|
|
746
1073
|
"""
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1074
|
+
else:
|
|
1075
|
+
# Max iterations reached for this task (for-else)
|
|
1076
|
+
print_warning_box(f"Max iterations ({args.max_iterations}) reached for current task")
|
|
1077
|
+
task_queue.mark_failed()
|
|
1078
|
+
session_entry = summarize_session(bug_input, "MAX_ITERATIONS", args.max_iterations)
|
|
1079
|
+
history_data["sessions"].append(session_entry)
|
|
1080
|
+
save_history(working_dir, history_data)
|
|
1081
|
+
|
|
1082
|
+
# Save iteration summary for this task
|
|
1083
|
+
LOG_DIR.mkdir(exist_ok=True)
|
|
1084
|
+
summary = f"# DaveLoop Session {session_id}\n\n"
|
|
1085
|
+
summary += f"Bug: {bug_input[:200]}...\n\n"
|
|
1086
|
+
summary += f"Iterations: {len(iteration_history)}\n\n"
|
|
1087
|
+
summary += "## Iteration History\n\n"
|
|
1088
|
+
for i, hist in enumerate(iteration_history, 1):
|
|
1089
|
+
summary += f"### Iteration {i}\n```\n{hist[:500]}...\n```\n\n"
|
|
1090
|
+
(LOG_DIR / f"{session_id}_summary.md").write_text(summary, encoding="utf-8")
|
|
1091
|
+
|
|
1092
|
+
# === All tasks done - print final summary ===
|
|
1093
|
+
input_monitor.stop()
|
|
1094
|
+
|
|
1095
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ ALL TASKS COMPLETE{C.RESET}")
|
|
1096
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
1097
|
+
for task in task_queue.all():
|
|
1098
|
+
desc = task["description"][:55]
|
|
1099
|
+
status = task["status"]
|
|
1100
|
+
if status == "done":
|
|
1101
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{desc}{C.RESET}")
|
|
1102
|
+
elif status == "failed":
|
|
1103
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.RED}{desc}{C.RESET}")
|
|
1104
|
+
else:
|
|
1105
|
+
print(f" {C.DIM}○ {desc}{C.RESET}")
|
|
752
1106
|
print()
|
|
753
1107
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
summary += f"Bug: {bug_input[:200]}...\n\n"
|
|
757
|
-
summary += f"Iterations: {args.max_iterations}\n\n"
|
|
758
|
-
summary += "## Iteration History\n\n"
|
|
759
|
-
for i, hist in enumerate(iteration_history, 1):
|
|
760
|
-
summary += f"### Iteration {i}\n```\n{hist[:500]}...\n```\n\n"
|
|
761
|
-
(LOG_DIR / f"{session_id}_summary.md").write_text(summary, encoding="utf-8")
|
|
1108
|
+
print(f" {C.DIM}Session: {session_id}{C.RESET}")
|
|
1109
|
+
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
762
1110
|
|
|
763
|
-
|
|
1111
|
+
# Return 0 if all tasks done, 1 if any failed
|
|
1112
|
+
all_done = all(t["status"] == "done" for t in task_queue.all())
|
|
1113
|
+
return 0 if all_done else 1
|
|
764
1114
|
|
|
765
1115
|
|
|
766
1116
|
if __name__ == "__main__":
|
daveloop-1.2.0.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
daveloop.py,sha256=7URdVP8E72feFkOkoRy1lloGqArUuoyUc3Vzcm00rww,34370
|
|
2
|
-
daveloop_swebench.py,sha256=iD9AU3XRiMQpt7TknFNlvnmPCNp64V-JaTfqTFgsGBM,15996
|
|
3
|
-
daveloop-1.2.0.dist-info/METADATA,sha256=PDmw8YwkHmb4anVrpVK-DSeRDfwLJGI9mY1LD3b5etQ,10285
|
|
4
|
-
daveloop-1.2.0.dist-info/WHEEL,sha256=hPN0AlP2dZM_3ZJZWP4WooepkmU9wzjGgCLCeFjkHLA,92
|
|
5
|
-
daveloop-1.2.0.dist-info/entry_points.txt,sha256=QcFAZgFrDfPtIikNQb7eW9DxOpBK7T-qWrKqbGAS9Ww,86
|
|
6
|
-
daveloop-1.2.0.dist-info/top_level.txt,sha256=36DiYt70m4DIK8t7IhV_y6hAzUIyeb5-qDUf3-gbDdg,27
|
|
7
|
-
daveloop-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|