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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: daveloop
3
- Version: 1.2.0
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.46.3)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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="?", help="Bug description or error message")
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
- # Get bug description
869
+ # Collect bug descriptions
870
+ bug_descriptions = []
634
871
  if args.file:
635
- bug_input = Path(args.file).read_text(encoding="utf-8")
872
+ bug_descriptions.append(Path(args.file).read_text(encoding="utf-8"))
636
873
  elif args.bug:
637
- bug_input = args.bug
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
- bug_input = sys.stdin.read().strip()
877
+ stdin_input = sys.stdin.read().strip()
878
+ if stdin_input:
879
+ bug_descriptions.append(stdin_input)
641
880
 
642
- if not bug_input:
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("Mode", "Autonomous", C.WHITE)
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
- print_section("BUG REPORT", C.BRIGHT_RED)
660
- # Wrap bug input nicely
661
- for line in bug_input.split('\n')[:8]:
662
- print(f" {C.BRIGHT_RED}{line[:70]}{C.RESET}")
663
- if len(bug_input.split('\n')) > 8:
664
- print(f" {C.RED}... +{len(bug_input.split(chr(10))) - 8} more lines{C.RESET}")
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
- sys.stdout.flush()
978
+ {bug_input}
979
+ {history_context}
667
980
 
668
- # Initial context
669
- context = f"""
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
- iteration_history = []
1006
+ iteration_history = []
681
1007
 
682
- for iteration in range(1, args.max_iterations + 1):
1008
+ # === INNER LOOP: iterations for current task ===
1009
+ for iteration in range(1, args.max_iterations + 1):
683
1010
 
684
- if iteration == 1:
685
- full_prompt = f"{system_prompt}\n\n---\n\n{context}"
686
- continue_session = False
687
- else:
688
- full_prompt = context
689
- continue_session = True
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
- if args.verbose:
692
- print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
1018
+ if args.verbose:
1019
+ print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
693
1020
 
694
- # Show "Claude is working" indicator
695
- print(f"\n {C.BRIGHT_BLUE}◆ Agent active...{C.RESET}\n")
696
- sys.stdout.flush()
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
- # Run Claude with real-time streaming output
699
- output = run_claude_code(full_prompt, working_dir, continue_session=continue_session, stream=True, timeout=args.timeout)
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
- print(f"\n{C.BRIGHT_BLUE} {'─' * 70}{C.RESET}")
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
- # Save log
704
- save_log(iteration, output, session_id)
705
- iteration_history.append(output)
1059
+ {human_input}
706
1060
 
707
- # Check exit condition
708
- signal, should_exit = check_exit_condition(output)
1061
+ Continue debugging with this corrected context. Use the reasoning protocol before each action.
1062
+ """
1063
+ continue
709
1064
 
710
- if should_exit:
711
- if signal == "RESOLVED":
712
- print_success_box("")
713
- print(f" {C.DIM}Session: {session_id}{C.RESET}")
714
- print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
715
- return 0
716
- elif signal == "CLARIFY":
717
- print_warning_box("Claude needs clarification")
718
- print(f"\n {C.BLUE}Your response:{C.RESET}")
719
- human_input = input(f" {C.WHITE}> {C.RESET}")
720
- context = f"""
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
- continue
728
- elif signal == "BLOCKED":
729
- print_error_box(f"Claude is blocked - needs human help")
730
- print_status("Session", session_id, C.WHITE)
731
- print_status("Logs", str(LOG_DIR), C.WHITE)
732
- print()
733
- return 1
734
- else:
735
- print_error_box(f"Error occurred: {signal}")
736
- return 1
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
- # Prepare context for next iteration
739
- context = f"""
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
- # Max iterations reached
749
- print_warning_box(f"Max iterations ({args.max_iterations}) reached")
750
- print_status("Session", session_id, C.WHITE)
751
- print_status("Logs", str(LOG_DIR), C.WHITE)
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
- # Save summary
755
- summary = f"# DaveLoop Session {session_id}\n\n"
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
- return 1
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__":
@@ -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,,