daveloop 1.2.0__py3-none-any.whl → 1.4.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.4.0.dist-info}/METADATA +10 -2
- daveloop-1.4.0.dist-info/RECORD +7 -0
- {daveloop-1.2.0.dist-info → daveloop-1.4.0.dist-info}/WHEEL +1 -1
- daveloop.py +528 -80
- daveloop-1.2.0.dist-info/RECORD +0 -7
- {daveloop-1.2.0.dist-info → daveloop-1.4.0.dist-info}/entry_points.txt +0 -0
- {daveloop-1.2.0.dist-info → daveloop-1.4.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.4.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=vO_mKj_kSciLmupY_GAw3qkRp4Axo6rsrTJx-lhFIZc,53540
|
|
2
|
+
daveloop_swebench.py,sha256=iD9AU3XRiMQpt7TknFNlvnmPCNp64V-JaTfqTFgsGBM,15996
|
|
3
|
+
daveloop-1.4.0.dist-info/METADATA,sha256=KFXheqH4I1_XexxlhPXlXnMRNzeYEVDnDKCGydjFqEg,10463
|
|
4
|
+
daveloop-1.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
daveloop-1.4.0.dist-info/entry_points.txt,sha256=QcFAZgFrDfPtIikNQb7eW9DxOpBK7T-qWrKqbGAS9Ww,86
|
|
6
|
+
daveloop-1.4.0.dist-info/top_level.txt,sha256=36DiYt70m4DIK8t7IhV_y6hAzUIyeb5-qDUf3-gbDdg,27
|
|
7
|
+
daveloop-1.4.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
|
|
|
@@ -19,6 +20,8 @@ MAX_ITERATIONS = 20
|
|
|
19
20
|
DEFAULT_TIMEOUT = 600 # 10 minutes in seconds
|
|
20
21
|
SCRIPT_DIR = Path(__file__).parent
|
|
21
22
|
PROMPT_FILE = SCRIPT_DIR / "daveloop_prompt.md"
|
|
23
|
+
MAESTRO_PROMPT_FILE = SCRIPT_DIR / "daveloop_maestro_prompt.md"
|
|
24
|
+
WEB_PROMPT_FILE = SCRIPT_DIR / "daveloop_web_prompt.md"
|
|
22
25
|
LOG_DIR = SCRIPT_DIR / "logs"
|
|
23
26
|
|
|
24
27
|
# Exit signals from Claude Code
|
|
@@ -195,6 +198,144 @@ class Spinner:
|
|
|
195
198
|
sys.stdout.write(f"\r {C.GREEN}✓{C.RESET} {self.message} complete {C.DIM}({elapsed:.1f}s){C.RESET} \n")
|
|
196
199
|
sys.stdout.flush()
|
|
197
200
|
|
|
201
|
+
# ============================================================================
|
|
202
|
+
# Task Queue
|
|
203
|
+
# ============================================================================
|
|
204
|
+
class TaskQueue:
|
|
205
|
+
"""Manages multiple bug tasks in sequence."""
|
|
206
|
+
|
|
207
|
+
def __init__(self):
|
|
208
|
+
self.tasks = [] # list of {"description": str, "status": "pending"|"active"|"done"|"failed"}
|
|
209
|
+
|
|
210
|
+
def add(self, description: str):
|
|
211
|
+
"""Add a new task with pending status."""
|
|
212
|
+
self.tasks.append({"description": description, "status": "pending"})
|
|
213
|
+
|
|
214
|
+
def next(self):
|
|
215
|
+
"""Find first pending task, set it to active, return it. None if no pending tasks."""
|
|
216
|
+
for task in self.tasks:
|
|
217
|
+
if task["status"] == "pending":
|
|
218
|
+
task["status"] = "active"
|
|
219
|
+
return task
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def current(self):
|
|
223
|
+
"""Return the task with status active, or None."""
|
|
224
|
+
for task in self.tasks:
|
|
225
|
+
if task["status"] == "active":
|
|
226
|
+
return task
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def mark_done(self):
|
|
230
|
+
"""Set current active task to done."""
|
|
231
|
+
task = self.current()
|
|
232
|
+
if task:
|
|
233
|
+
task["status"] = "done"
|
|
234
|
+
|
|
235
|
+
def mark_failed(self):
|
|
236
|
+
"""Set current active task to failed."""
|
|
237
|
+
task = self.current()
|
|
238
|
+
if task:
|
|
239
|
+
task["status"] = "failed"
|
|
240
|
+
|
|
241
|
+
def remaining(self) -> int:
|
|
242
|
+
"""Count of pending tasks."""
|
|
243
|
+
return sum(1 for t in self.tasks if t["status"] == "pending")
|
|
244
|
+
|
|
245
|
+
def all(self):
|
|
246
|
+
"""Return all tasks."""
|
|
247
|
+
return self.tasks
|
|
248
|
+
|
|
249
|
+
def summary_display(self):
|
|
250
|
+
"""Print a nice box showing all tasks with status icons."""
|
|
251
|
+
active_count = sum(1 for t in self.tasks if t["status"] == "active")
|
|
252
|
+
done_count = sum(1 for t in self.tasks if t["status"] == "done")
|
|
253
|
+
total = len(self.tasks)
|
|
254
|
+
active_idx = next((i for i, t in enumerate(self.tasks) if t["status"] == "active"), 0)
|
|
255
|
+
|
|
256
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ TASK QUEUE ({active_idx + 1}/{total} active){C.RESET}")
|
|
257
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
258
|
+
for task in self.tasks:
|
|
259
|
+
desc = task["description"][:50]
|
|
260
|
+
if task["status"] == "done":
|
|
261
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{desc}{C.RESET}")
|
|
262
|
+
elif task["status"] == "active":
|
|
263
|
+
print(f" {C.BRIGHT_CYAN}▶{C.RESET} {C.BRIGHT_WHITE}{desc}{C.RESET} {C.DIM}(active){C.RESET}")
|
|
264
|
+
elif task["status"] == "pending":
|
|
265
|
+
print(f" {C.DIM}○{C.RESET} {C.DIM}{desc}{C.RESET} {C.DIM}(pending){C.RESET}")
|
|
266
|
+
elif task["status"] == "failed":
|
|
267
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.RED}{desc}{C.RESET}")
|
|
268
|
+
print()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ============================================================================
|
|
272
|
+
# Session Memory
|
|
273
|
+
# ============================================================================
|
|
274
|
+
def load_history(working_dir: str) -> dict:
|
|
275
|
+
"""Read .daveloop_history.json from working_dir. Return default if missing or corrupted."""
|
|
276
|
+
history_file = Path(working_dir) / ".daveloop_history.json"
|
|
277
|
+
if not history_file.exists():
|
|
278
|
+
return {"sessions": []}
|
|
279
|
+
try:
|
|
280
|
+
data = json.loads(history_file.read_text(encoding="utf-8"))
|
|
281
|
+
if not isinstance(data, dict) or "sessions" not in data:
|
|
282
|
+
print_warning_box("Corrupted history file - resetting")
|
|
283
|
+
return {"sessions": []}
|
|
284
|
+
return data
|
|
285
|
+
except (json.JSONDecodeError, ValueError):
|
|
286
|
+
print_warning_box("Corrupted history JSON - resetting")
|
|
287
|
+
return {"sessions": []}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def save_history(working_dir: str, history_data: dict):
|
|
291
|
+
"""Write to .daveloop_history.json. Keep only last 20 sessions."""
|
|
292
|
+
history_file = Path(working_dir) / ".daveloop_history.json"
|
|
293
|
+
history_data["sessions"] = history_data["sessions"][-20:]
|
|
294
|
+
history_file.write_text(json.dumps(history_data, indent=2), encoding="utf-8")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def summarize_session(bug: str, outcome: str, iterations: int) -> dict:
|
|
298
|
+
"""Return a dict summarizing a session."""
|
|
299
|
+
now = datetime.now()
|
|
300
|
+
return {
|
|
301
|
+
"session_id": now.strftime("%Y%m%d_%H%M%S"),
|
|
302
|
+
"bug": bug,
|
|
303
|
+
"outcome": outcome,
|
|
304
|
+
"iterations": iterations,
|
|
305
|
+
"timestamp": now.isoformat()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def format_history_context(sessions: list) -> str:
|
|
310
|
+
"""Return markdown string summarizing recent sessions for Claude context."""
|
|
311
|
+
if not sessions:
|
|
312
|
+
return ""
|
|
313
|
+
lines = ["## Previous DaveLoop Sessions"]
|
|
314
|
+
for s in sessions[-10:]: # Show last 10
|
|
315
|
+
outcome = s.get("outcome", "UNKNOWN")
|
|
316
|
+
bug = s.get("bug", "unknown")[:60]
|
|
317
|
+
iters = s.get("iterations", "?")
|
|
318
|
+
lines.append(f"- [{outcome}] \"{bug}\" ({iters} iterations)")
|
|
319
|
+
return "\n".join(lines)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def print_history_box(sessions: list):
|
|
323
|
+
"""Print a nice UI box showing loaded history."""
|
|
324
|
+
if not sessions:
|
|
325
|
+
return
|
|
326
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ SESSION HISTORY{C.RESET}")
|
|
327
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
328
|
+
for s in sessions[-10:]:
|
|
329
|
+
outcome = s.get("outcome", "UNKNOWN")
|
|
330
|
+
bug = s.get("bug", "unknown")[:55]
|
|
331
|
+
iters = s.get("iterations", "?")
|
|
332
|
+
if outcome == "RESOLVED":
|
|
333
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
334
|
+
else:
|
|
335
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.WHITE}{bug}{C.RESET} {C.DIM}({iters} iter){C.RESET}")
|
|
336
|
+
print()
|
|
337
|
+
|
|
338
|
+
|
|
198
339
|
# ============================================================================
|
|
199
340
|
# Output Formatter
|
|
200
341
|
# ============================================================================
|
|
@@ -274,6 +415,74 @@ def format_claude_output(output: str) -> str:
|
|
|
274
415
|
|
|
275
416
|
return '\n'.join(formatted)
|
|
276
417
|
|
|
418
|
+
# ============================================================================
|
|
419
|
+
# Input Monitor
|
|
420
|
+
# ============================================================================
|
|
421
|
+
class InputMonitor:
|
|
422
|
+
"""Daemon thread that reads stdin for commands while Claude is running.
|
|
423
|
+
|
|
424
|
+
After detecting a command, stops reading stdin so that input() calls
|
|
425
|
+
in the main thread can safely read without a race condition.
|
|
426
|
+
Call resume_reading() after the main thread is done with input().
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
VALID_COMMANDS = ("wait", "pause", "add", "done")
|
|
430
|
+
|
|
431
|
+
def __init__(self):
|
|
432
|
+
self._command = None
|
|
433
|
+
self._lock = threading.Lock()
|
|
434
|
+
self._thread = threading.Thread(target=self._read_loop, daemon=True)
|
|
435
|
+
self._running = False
|
|
436
|
+
self._read_gate = threading.Event()
|
|
437
|
+
self._read_gate.set() # Start with reading enabled
|
|
438
|
+
|
|
439
|
+
def start(self):
|
|
440
|
+
"""Start monitoring stdin."""
|
|
441
|
+
self._running = True
|
|
442
|
+
self._thread.start()
|
|
443
|
+
|
|
444
|
+
def stop(self):
|
|
445
|
+
"""Stop monitoring stdin."""
|
|
446
|
+
self._running = False
|
|
447
|
+
self._read_gate.set() # Unblock the thread so it can exit
|
|
448
|
+
|
|
449
|
+
def resume_reading(self):
|
|
450
|
+
"""Resume reading stdin after an interrupt has been handled."""
|
|
451
|
+
self._read_gate.set()
|
|
452
|
+
|
|
453
|
+
def _read_loop(self):
|
|
454
|
+
"""Read lines from stdin, looking for valid commands."""
|
|
455
|
+
while self._running:
|
|
456
|
+
# Wait until reading is enabled (blocks after a command is detected)
|
|
457
|
+
self._read_gate.wait()
|
|
458
|
+
if not self._running:
|
|
459
|
+
break
|
|
460
|
+
try:
|
|
461
|
+
line = sys.stdin.readline()
|
|
462
|
+
if not line:
|
|
463
|
+
break
|
|
464
|
+
cmd = line.strip().lower()
|
|
465
|
+
if cmd in self.VALID_COMMANDS:
|
|
466
|
+
with self._lock:
|
|
467
|
+
self._command = cmd
|
|
468
|
+
# Stop reading so input() in the main thread has no competition
|
|
469
|
+
self._read_gate.clear()
|
|
470
|
+
except (EOFError, OSError):
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
def has_command(self) -> bool:
|
|
474
|
+
"""Check if a command has been received."""
|
|
475
|
+
with self._lock:
|
|
476
|
+
return self._command is not None
|
|
477
|
+
|
|
478
|
+
def get_command(self) -> str:
|
|
479
|
+
"""Get and clear the current command."""
|
|
480
|
+
with self._lock:
|
|
481
|
+
cmd = self._command
|
|
482
|
+
self._command = None
|
|
483
|
+
return cmd
|
|
484
|
+
|
|
485
|
+
|
|
277
486
|
# ============================================================================
|
|
278
487
|
# Core Functions
|
|
279
488
|
# ============================================================================
|
|
@@ -286,6 +495,24 @@ def load_prompt() -> str:
|
|
|
286
495
|
return "You are debugging. Fix the bug. Output [DAVELOOP:RESOLVED] when done."
|
|
287
496
|
|
|
288
497
|
|
|
498
|
+
def load_maestro_prompt() -> str:
|
|
499
|
+
"""Load the Maestro mobile testing prompt."""
|
|
500
|
+
if MAESTRO_PROMPT_FILE.exists():
|
|
501
|
+
return MAESTRO_PROMPT_FILE.read_text(encoding="utf-8")
|
|
502
|
+
else:
|
|
503
|
+
print_warning_box(f"Maestro prompt file not found: {MAESTRO_PROMPT_FILE}")
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def load_web_prompt() -> str:
|
|
508
|
+
"""Load the Web UI testing prompt."""
|
|
509
|
+
if WEB_PROMPT_FILE.exists():
|
|
510
|
+
return WEB_PROMPT_FILE.read_text(encoding="utf-8")
|
|
511
|
+
else:
|
|
512
|
+
print_warning_box(f"Web prompt file not found: {WEB_PROMPT_FILE}")
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
|
|
289
516
|
def find_claude_cli():
|
|
290
517
|
"""Find Claude CLI executable path."""
|
|
291
518
|
import platform
|
|
@@ -327,11 +554,12 @@ def find_claude_cli():
|
|
|
327
554
|
return None
|
|
328
555
|
|
|
329
556
|
|
|
330
|
-
def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool = False, stream: bool = True, timeout: int = DEFAULT_TIMEOUT) -> str:
|
|
557
|
+
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
558
|
"""Execute Claude Code CLI with the given prompt.
|
|
332
559
|
|
|
333
560
|
If stream=True, output is printed in real-time and also returned.
|
|
334
561
|
timeout is in seconds (default 600 = 10 minutes).
|
|
562
|
+
input_monitor: optional InputMonitor to check for user commands during execution.
|
|
335
563
|
"""
|
|
336
564
|
claude_cmd = find_claude_cli()
|
|
337
565
|
if not claude_cmd:
|
|
@@ -375,7 +603,6 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
375
603
|
start_time = time.time()
|
|
376
604
|
|
|
377
605
|
# Read and display JSON stream output
|
|
378
|
-
import json
|
|
379
606
|
output_lines = []
|
|
380
607
|
full_text = []
|
|
381
608
|
|
|
@@ -497,12 +724,9 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
497
724
|
print(f" {C.BRIGHT_BLUE}└─{C.RESET} {C.GREEN}✓{C.RESET}")
|
|
498
725
|
|
|
499
726
|
elif msg_type == "result":
|
|
500
|
-
# Final result
|
|
727
|
+
# Final result - skip printing as it duplicates streamed content
|
|
501
728
|
text = data.get("result", "")
|
|
502
729
|
if text:
|
|
503
|
-
for line_text in text.split('\n'):
|
|
504
|
-
formatted = format_output_line(line_text)
|
|
505
|
-
print(formatted)
|
|
506
730
|
full_text.append(text)
|
|
507
731
|
|
|
508
732
|
elif msg_type == "error":
|
|
@@ -518,6 +742,16 @@ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool
|
|
|
518
742
|
|
|
519
743
|
output_lines.append(line)
|
|
520
744
|
|
|
745
|
+
# Check for user commands from InputMonitor
|
|
746
|
+
if input_monitor and input_monitor.has_command():
|
|
747
|
+
user_cmd = input_monitor.get_command()
|
|
748
|
+
try:
|
|
749
|
+
process.terminate()
|
|
750
|
+
process.wait(timeout=5)
|
|
751
|
+
except Exception:
|
|
752
|
+
pass
|
|
753
|
+
return f"[DAVELOOP:INTERRUPTED:{user_cmd}]"
|
|
754
|
+
|
|
521
755
|
process.wait(timeout=timeout)
|
|
522
756
|
return '\n'.join(full_text)
|
|
523
757
|
else:
|
|
@@ -616,13 +850,15 @@ def main():
|
|
|
616
850
|
description="DaveLoop - Self-Healing Debug Agent",
|
|
617
851
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
618
852
|
)
|
|
619
|
-
parser.add_argument("bug", nargs="
|
|
853
|
+
parser.add_argument("bug", nargs="*", help="Bug description(s) or error message(s)")
|
|
620
854
|
parser.add_argument("-f", "--file", help="Read bug description from file")
|
|
621
855
|
parser.add_argument("-d", "--dir", help="Working directory for Claude Code")
|
|
622
856
|
parser.add_argument("-m", "--max-iterations", type=int, default=MAX_ITERATIONS)
|
|
623
857
|
parser.add_argument("-t", "--timeout", type=int, default=DEFAULT_TIMEOUT,
|
|
624
858
|
help="Timeout per iteration in seconds (default: 600)")
|
|
625
859
|
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
860
|
+
parser.add_argument("--maestro", action="store_true", help="Enable Maestro mobile testing mode")
|
|
861
|
+
parser.add_argument("--web", action="store_true", help="Enable Playwright web UI testing mode")
|
|
626
862
|
|
|
627
863
|
args = parser.parse_args()
|
|
628
864
|
|
|
@@ -630,46 +866,136 @@ def main():
|
|
|
630
866
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
631
867
|
print(BANNER)
|
|
632
868
|
|
|
633
|
-
#
|
|
869
|
+
# Collect bug descriptions
|
|
870
|
+
bug_descriptions = []
|
|
634
871
|
if args.file:
|
|
635
|
-
|
|
872
|
+
bug_descriptions.append(Path(args.file).read_text(encoding="utf-8"))
|
|
636
873
|
elif args.bug:
|
|
637
|
-
|
|
874
|
+
bug_descriptions.extend(args.bug)
|
|
638
875
|
else:
|
|
639
876
|
print(f" {C.CYAN}Describe the bug (Ctrl+D or Ctrl+Z to finish):{C.RESET}")
|
|
640
|
-
|
|
877
|
+
stdin_input = sys.stdin.read().strip()
|
|
878
|
+
if stdin_input:
|
|
879
|
+
bug_descriptions.append(stdin_input)
|
|
641
880
|
|
|
642
|
-
if not
|
|
881
|
+
if not bug_descriptions:
|
|
643
882
|
print_error_box("No bug description provided")
|
|
644
883
|
return 1
|
|
645
884
|
|
|
646
885
|
# Setup
|
|
647
886
|
session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
648
887
|
system_prompt = load_prompt()
|
|
888
|
+
if args.maestro:
|
|
889
|
+
maestro_prompt = load_maestro_prompt()
|
|
890
|
+
if maestro_prompt:
|
|
891
|
+
system_prompt = system_prompt + "\n\n---\n\n" + maestro_prompt
|
|
892
|
+
elif args.web:
|
|
893
|
+
web_prompt = load_web_prompt()
|
|
894
|
+
if web_prompt:
|
|
895
|
+
system_prompt = system_prompt + "\n\n---\n\n" + web_prompt
|
|
649
896
|
working_dir = args.dir or os.getcwd()
|
|
650
897
|
|
|
898
|
+
# Load session history
|
|
899
|
+
history_data = load_history(working_dir)
|
|
900
|
+
if history_data["sessions"]:
|
|
901
|
+
print_history_box(history_data["sessions"])
|
|
902
|
+
|
|
651
903
|
# Session info
|
|
652
904
|
print_header_box(f"SESSION: {session_id}", C.BRIGHT_BLUE)
|
|
653
905
|
print_status("Directory", working_dir, C.WHITE)
|
|
654
906
|
print_status("Iterations", str(args.max_iterations), C.WHITE)
|
|
655
907
|
print_status("Timeout", f"{args.timeout // 60}m per iteration", C.WHITE)
|
|
656
|
-
print_status("
|
|
908
|
+
print_status("Tasks", str(len(bug_descriptions)), C.WHITE)
|
|
909
|
+
mode_name = "Maestro Mobile Testing" if args.maestro else "Playwright Web Testing" if args.web else "Autonomous"
|
|
910
|
+
print_status("Mode", mode_name, C.WHITE)
|
|
657
911
|
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
658
912
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
for
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
913
|
+
# Build task queue
|
|
914
|
+
task_queue = TaskQueue()
|
|
915
|
+
for desc in bug_descriptions:
|
|
916
|
+
task_queue.add(desc)
|
|
917
|
+
|
|
918
|
+
# Print controls hint
|
|
919
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}┌─ CONTROLS {'─' * 58}┐{C.RESET}")
|
|
920
|
+
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}")
|
|
921
|
+
print(f"{C.BRIGHT_BLUE}└{'─' * 70}┘{C.RESET}")
|
|
922
|
+
|
|
923
|
+
# Start input monitor
|
|
924
|
+
input_monitor = InputMonitor()
|
|
925
|
+
input_monitor.start()
|
|
926
|
+
|
|
927
|
+
# Build history context for initial prompt
|
|
928
|
+
history_context = ""
|
|
929
|
+
if history_data["sessions"]:
|
|
930
|
+
history_context = "\n\n" + format_history_context(history_data["sessions"])
|
|
931
|
+
|
|
932
|
+
# === OUTER LOOP: iterate over tasks ===
|
|
933
|
+
while True:
|
|
934
|
+
task = task_queue.next()
|
|
935
|
+
if task is None:
|
|
936
|
+
break
|
|
937
|
+
|
|
938
|
+
bug_input = task["description"]
|
|
939
|
+
task_queue.summary_display()
|
|
940
|
+
|
|
941
|
+
if args.maestro:
|
|
942
|
+
print_section("MAESTRO TASK", C.BRIGHT_CYAN)
|
|
943
|
+
section_color = C.BRIGHT_CYAN
|
|
944
|
+
elif args.web:
|
|
945
|
+
print_section("WEB UI TASK", C.BRIGHT_MAGENTA)
|
|
946
|
+
section_color = C.BRIGHT_MAGENTA
|
|
947
|
+
else:
|
|
948
|
+
print_section("BUG REPORT", C.BRIGHT_RED)
|
|
949
|
+
section_color = C.BRIGHT_RED
|
|
950
|
+
for line in bug_input.split('\n')[:8]:
|
|
951
|
+
print(f" {section_color}{line[:70]}{C.RESET}")
|
|
952
|
+
if len(bug_input.split('\n')) > 8:
|
|
953
|
+
print(f" {section_color}... +{len(bug_input.split(chr(10))) - 8} more lines{C.RESET}")
|
|
954
|
+
sys.stdout.flush()
|
|
955
|
+
|
|
956
|
+
# Initial context for this task
|
|
957
|
+
if args.maestro:
|
|
958
|
+
context = f"""
|
|
959
|
+
## Maestro Mobile Testing Task
|
|
960
|
+
|
|
961
|
+
{bug_input}
|
|
962
|
+
{history_context}
|
|
963
|
+
|
|
964
|
+
## Instructions
|
|
965
|
+
|
|
966
|
+
1. First, detect connected devices/emulators (run `adb devices` and/or `xcrun simctl list devices available`)
|
|
967
|
+
2. If no device is found, auto-launch an emulator/simulator
|
|
968
|
+
3. Ensure the target app is installed on the device
|
|
969
|
+
4. Proceed with the Maestro testing task described above
|
|
970
|
+
5. Before declaring success, verify by running the flow(s) 3 consecutive times - all must pass
|
|
971
|
+
|
|
972
|
+
Use the reasoning protocol before each action.
|
|
973
|
+
"""
|
|
974
|
+
elif args.web:
|
|
975
|
+
context = f"""
|
|
976
|
+
## Web UI Testing Task
|
|
665
977
|
|
|
666
|
-
|
|
978
|
+
{bug_input}
|
|
979
|
+
{history_context}
|
|
667
980
|
|
|
668
|
-
|
|
669
|
-
|
|
981
|
+
## Instructions
|
|
982
|
+
|
|
983
|
+
1. First, explore the project to detect the framework and find the dev server command
|
|
984
|
+
2. Install Playwright if not already installed (`npm install -D @playwright/test && npx playwright install chromium`)
|
|
985
|
+
3. Start the dev server if not already running
|
|
986
|
+
4. Read the source code to understand the UI components, especially any gesture/drag/interactive elements
|
|
987
|
+
5. Write Playwright tests in an `e2e/` directory that test the app like a real human would - use actual mouse movements, drags, clicks, hovers, keyboard input
|
|
988
|
+
6. Test gestures and buttons SEPARATELY - a working button does not prove the gesture works
|
|
989
|
+
7. Before declaring success, verify by running the tests 3 consecutive times - all must pass
|
|
990
|
+
|
|
991
|
+
Use the reasoning protocol before each action.
|
|
992
|
+
"""
|
|
993
|
+
else:
|
|
994
|
+
context = f"""
|
|
670
995
|
## Bug Report
|
|
671
996
|
|
|
672
997
|
{bug_input}
|
|
998
|
+
{history_context}
|
|
673
999
|
|
|
674
1000
|
## Instructions
|
|
675
1001
|
|
|
@@ -677,66 +1003,165 @@ Analyze this bug. Gather whatever logs/information you need to understand it.
|
|
|
677
1003
|
Then fix it. Use the reasoning protocol before each action.
|
|
678
1004
|
"""
|
|
679
1005
|
|
|
680
|
-
|
|
1006
|
+
iteration_history = []
|
|
681
1007
|
|
|
682
|
-
|
|
1008
|
+
# === INNER LOOP: iterations for current task ===
|
|
1009
|
+
for iteration in range(1, args.max_iterations + 1):
|
|
683
1010
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1011
|
+
if iteration == 1:
|
|
1012
|
+
full_prompt = f"{system_prompt}\n\n---\n\n{context}"
|
|
1013
|
+
continue_session = False
|
|
1014
|
+
else:
|
|
1015
|
+
full_prompt = context
|
|
1016
|
+
continue_session = True
|
|
690
1017
|
|
|
691
|
-
|
|
692
|
-
|
|
1018
|
+
if args.verbose:
|
|
1019
|
+
print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
|
|
693
1020
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1021
|
+
# Show "Claude is working" indicator
|
|
1022
|
+
print(f"\n {C.BRIGHT_BLUE}◆ Agent active...{C.RESET}\n")
|
|
1023
|
+
sys.stdout.flush()
|
|
697
1024
|
|
|
698
|
-
|
|
699
|
-
|
|
1025
|
+
# Run Claude with real-time streaming output
|
|
1026
|
+
output = run_claude_code(
|
|
1027
|
+
full_prompt, working_dir,
|
|
1028
|
+
continue_session=continue_session,
|
|
1029
|
+
stream=True, timeout=args.timeout,
|
|
1030
|
+
input_monitor=input_monitor
|
|
1031
|
+
)
|
|
700
1032
|
|
|
701
|
-
|
|
1033
|
+
print(f"\n{C.BRIGHT_BLUE} {'─' * 70}{C.RESET}")
|
|
1034
|
+
|
|
1035
|
+
# Save log
|
|
1036
|
+
save_log(iteration, output, session_id)
|
|
1037
|
+
iteration_history.append(output)
|
|
1038
|
+
|
|
1039
|
+
# Check for user interrupt commands
|
|
1040
|
+
if "[DAVELOOP:INTERRUPTED:" in output:
|
|
1041
|
+
# Extract the command name
|
|
1042
|
+
cmd_start = output.index("[DAVELOOP:INTERRUPTED:") + len("[DAVELOOP:INTERRUPTED:")
|
|
1043
|
+
cmd_end = output.index("]", cmd_start)
|
|
1044
|
+
user_cmd = output[cmd_start:cmd_end]
|
|
1045
|
+
|
|
1046
|
+
if user_cmd in ("wait", "pause"):
|
|
1047
|
+
# Pause and get user correction
|
|
1048
|
+
print(f"\n{C.BRIGHT_YELLOW}{C.BOLD} \u23f8 PAUSED - DaveLoop is waiting for your input{C.RESET}")
|
|
1049
|
+
print(f"{C.BRIGHT_YELLOW} {'─' * 70}{C.RESET}")
|
|
1050
|
+
print(f" {C.WHITE} Type your correction or additional context:{C.RESET}")
|
|
1051
|
+
try:
|
|
1052
|
+
human_input = input(f" {C.WHITE}> {C.RESET}")
|
|
1053
|
+
except EOFError:
|
|
1054
|
+
human_input = ""
|
|
1055
|
+
input_monitor.resume_reading()
|
|
1056
|
+
context = f"""
|
|
1057
|
+
## Human Correction (pause/wait command)
|
|
702
1058
|
|
|
703
|
-
|
|
704
|
-
save_log(iteration, output, session_id)
|
|
705
|
-
iteration_history.append(output)
|
|
1059
|
+
{human_input}
|
|
706
1060
|
|
|
707
|
-
|
|
708
|
-
|
|
1061
|
+
Continue debugging with this corrected context. Use the reasoning protocol before each action.
|
|
1062
|
+
"""
|
|
1063
|
+
continue
|
|
709
1064
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1065
|
+
elif user_cmd == "add":
|
|
1066
|
+
# Prompt for new task, then resume current
|
|
1067
|
+
print(f"\n {C.BRIGHT_CYAN}Enter new task description:{C.RESET}")
|
|
1068
|
+
try:
|
|
1069
|
+
new_desc = input(f" {C.WHITE}> {C.RESET}")
|
|
1070
|
+
except EOFError:
|
|
1071
|
+
new_desc = ""
|
|
1072
|
+
input_monitor.resume_reading()
|
|
1073
|
+
if new_desc.strip():
|
|
1074
|
+
task_queue.add(new_desc.strip())
|
|
1075
|
+
print(f" {C.GREEN}✓{C.RESET} Task added to queue")
|
|
1076
|
+
task_queue.summary_display()
|
|
1077
|
+
# Resume current task with --continue
|
|
1078
|
+
context = f"""
|
|
1079
|
+
## Continuing after user added a new task to the queue
|
|
1080
|
+
|
|
1081
|
+
Continue the current debugging task. Use the reasoning protocol before each action.
|
|
1082
|
+
"""
|
|
1083
|
+
continue
|
|
1084
|
+
|
|
1085
|
+
elif user_cmd == "done":
|
|
1086
|
+
# Clean exit
|
|
1087
|
+
input_monitor.stop()
|
|
1088
|
+
session_entry = summarize_session(bug_input, "DONE_BY_USER", iteration)
|
|
1089
|
+
history_data["sessions"].append(session_entry)
|
|
1090
|
+
save_history(working_dir, history_data)
|
|
1091
|
+
print(f"\n {C.GREEN}✓{C.RESET} Session saved. Exiting by user request.")
|
|
1092
|
+
return 0
|
|
1093
|
+
|
|
1094
|
+
# Check exit condition
|
|
1095
|
+
signal, should_exit = check_exit_condition(output)
|
|
1096
|
+
|
|
1097
|
+
if should_exit:
|
|
1098
|
+
if signal == "RESOLVED":
|
|
1099
|
+
print_success_box("")
|
|
1100
|
+
print(f" {C.DIM}Session: {session_id}{C.RESET}")
|
|
1101
|
+
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
1102
|
+
task_queue.mark_done()
|
|
1103
|
+
session_entry = summarize_session(bug_input, "RESOLVED", iteration)
|
|
1104
|
+
history_data["sessions"].append(session_entry)
|
|
1105
|
+
save_history(working_dir, history_data)
|
|
1106
|
+
break # Move to next task
|
|
1107
|
+
elif signal == "CLARIFY":
|
|
1108
|
+
print_warning_box("Claude needs clarification")
|
|
1109
|
+
print(f"\n {C.BLUE}Your response:{C.RESET}")
|
|
1110
|
+
try:
|
|
1111
|
+
human_input = input(f" {C.WHITE}> {C.RESET}")
|
|
1112
|
+
except EOFError:
|
|
1113
|
+
human_input = ""
|
|
1114
|
+
input_monitor.resume_reading()
|
|
1115
|
+
context = f"""
|
|
721
1116
|
## Human Clarification
|
|
722
1117
|
|
|
723
1118
|
{human_input}
|
|
724
1119
|
|
|
725
1120
|
Continue debugging with this information. Use the reasoning protocol before each action.
|
|
726
1121
|
"""
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1122
|
+
continue
|
|
1123
|
+
elif signal == "BLOCKED":
|
|
1124
|
+
print_error_box("Claude is blocked - needs human help")
|
|
1125
|
+
print_status("Session", session_id, C.WHITE)
|
|
1126
|
+
print_status("Logs", str(LOG_DIR), C.WHITE)
|
|
1127
|
+
print()
|
|
1128
|
+
task_queue.mark_failed()
|
|
1129
|
+
session_entry = summarize_session(bug_input, "BLOCKED", iteration)
|
|
1130
|
+
history_data["sessions"].append(session_entry)
|
|
1131
|
+
save_history(working_dir, history_data)
|
|
1132
|
+
break # Move to next task
|
|
1133
|
+
else:
|
|
1134
|
+
print_error_box(f"Error occurred: {signal}")
|
|
1135
|
+
task_queue.mark_failed()
|
|
1136
|
+
session_entry = summarize_session(bug_input, "ERROR", iteration)
|
|
1137
|
+
history_data["sessions"].append(session_entry)
|
|
1138
|
+
save_history(working_dir, history_data)
|
|
1139
|
+
break # Move to next task
|
|
1140
|
+
|
|
1141
|
+
# Prepare context for next iteration
|
|
1142
|
+
if args.maestro:
|
|
1143
|
+
context = f"""
|
|
1144
|
+
## Iteration {iteration + 1}
|
|
1145
|
+
|
|
1146
|
+
The Maestro flow(s) are NOT yet passing reliably. You have full context from previous iterations.
|
|
1147
|
+
|
|
1148
|
+
Continue working on the flows. Check device status, inspect the UI hierarchy, fix selectors or timing issues, and re-run.
|
|
1149
|
+
Remember: all flows must pass 3 consecutive times before resolving.
|
|
1150
|
+
Use the reasoning protocol before each action.
|
|
1151
|
+
"""
|
|
1152
|
+
elif args.web:
|
|
1153
|
+
context = f"""
|
|
1154
|
+
## Iteration {iteration + 1}
|
|
1155
|
+
|
|
1156
|
+
The Playwright tests are NOT yet passing reliably. You have full context from previous iterations.
|
|
737
1157
|
|
|
738
|
-
|
|
739
|
-
|
|
1158
|
+
Continue working on the tests. Check selectors, timing, server status, and re-run.
|
|
1159
|
+
Make sure you are testing like a real human - use actual mouse gestures, not just button clicks.
|
|
1160
|
+
Remember: all tests must pass 3 consecutive times before resolving.
|
|
1161
|
+
Use the reasoning protocol before each action.
|
|
1162
|
+
"""
|
|
1163
|
+
else:
|
|
1164
|
+
context = f"""
|
|
740
1165
|
## Iteration {iteration + 1}
|
|
741
1166
|
|
|
742
1167
|
The bug is NOT yet resolved. You have full context from previous iterations.
|
|
@@ -744,23 +1169,46 @@ The bug is NOT yet resolved. You have full context from previous iterations.
|
|
|
744
1169
|
Continue debugging. Analyze what happened, determine next steps, and proceed.
|
|
745
1170
|
Use the reasoning protocol before each action.
|
|
746
1171
|
"""
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1172
|
+
else:
|
|
1173
|
+
# Max iterations reached for this task (for-else)
|
|
1174
|
+
print_warning_box(f"Max iterations ({args.max_iterations}) reached for current task")
|
|
1175
|
+
task_queue.mark_failed()
|
|
1176
|
+
session_entry = summarize_session(bug_input, "MAX_ITERATIONS", args.max_iterations)
|
|
1177
|
+
history_data["sessions"].append(session_entry)
|
|
1178
|
+
save_history(working_dir, history_data)
|
|
1179
|
+
|
|
1180
|
+
# Save iteration summary for this task
|
|
1181
|
+
LOG_DIR.mkdir(exist_ok=True)
|
|
1182
|
+
summary = f"# DaveLoop Session {session_id}\n\n"
|
|
1183
|
+
summary += f"Bug: {bug_input[:200]}...\n\n"
|
|
1184
|
+
summary += f"Iterations: {len(iteration_history)}\n\n"
|
|
1185
|
+
summary += "## Iteration History\n\n"
|
|
1186
|
+
for i, hist in enumerate(iteration_history, 1):
|
|
1187
|
+
summary += f"### Iteration {i}\n```\n{hist[:500]}...\n```\n\n"
|
|
1188
|
+
(LOG_DIR / f"{session_id}_summary.md").write_text(summary, encoding="utf-8")
|
|
1189
|
+
|
|
1190
|
+
# === All tasks done - print final summary ===
|
|
1191
|
+
input_monitor.stop()
|
|
1192
|
+
|
|
1193
|
+
print(f"\n{C.BRIGHT_BLUE}{C.BOLD}◆ ALL TASKS COMPLETE{C.RESET}")
|
|
1194
|
+
print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}")
|
|
1195
|
+
for task in task_queue.all():
|
|
1196
|
+
desc = task["description"][:55]
|
|
1197
|
+
status = task["status"]
|
|
1198
|
+
if status == "done":
|
|
1199
|
+
print(f" {C.BRIGHT_GREEN}✓{C.RESET} {C.WHITE}{desc}{C.RESET}")
|
|
1200
|
+
elif status == "failed":
|
|
1201
|
+
print(f" {C.BRIGHT_RED}✗{C.RESET} {C.RED}{desc}{C.RESET}")
|
|
1202
|
+
else:
|
|
1203
|
+
print(f" {C.DIM}○ {desc}{C.RESET}")
|
|
752
1204
|
print()
|
|
753
1205
|
|
|
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")
|
|
1206
|
+
print(f" {C.DIM}Session: {session_id}{C.RESET}")
|
|
1207
|
+
print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
|
|
762
1208
|
|
|
763
|
-
|
|
1209
|
+
# Return 0 if all tasks done, 1 if any failed
|
|
1210
|
+
all_done = all(t["status"] == "done" for t in task_queue.all())
|
|
1211
|
+
return 0 if all_done else 1
|
|
764
1212
|
|
|
765
1213
|
|
|
766
1214
|
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
|