daveloop 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.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
 
@@ -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.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
 
@@ -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="?", help="Bug description or error message")
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
- # Get bug description
847
+ # Collect bug descriptions
848
+ bug_descriptions = []
634
849
  if args.file:
635
- bug_input = Path(args.file).read_text(encoding="utf-8")
850
+ bug_descriptions.append(Path(args.file).read_text(encoding="utf-8"))
636
851
  elif args.bug:
637
- bug_input = args.bug
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
- bug_input = sys.stdin.read().strip()
855
+ stdin_input = sys.stdin.read().strip()
856
+ if stdin_input:
857
+ bug_descriptions.append(stdin_input)
641
858
 
642
- if not bug_input:
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
- 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}")
882
+ # Build task queue
883
+ task_queue = TaskQueue()
884
+ for desc in bug_descriptions:
885
+ task_queue.add(desc)
665
886
 
666
- sys.stdout.flush()
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
- # Initial context
669
- context = f"""
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
- iteration_history = []
930
+ iteration_history = []
681
931
 
682
- for iteration in range(1, args.max_iterations + 1):
932
+ # === INNER LOOP: iterations for current task ===
933
+ for iteration in range(1, args.max_iterations + 1):
683
934
 
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
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
- if args.verbose:
692
- print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
942
+ if args.verbose:
943
+ print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
693
944
 
694
- # Show "Claude is working" indicator
695
- print(f"\n {C.BRIGHT_BLUE}◆ Agent active...{C.RESET}\n")
696
- sys.stdout.flush()
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
- # 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)
700
-
701
- print(f"\n{C.BRIGHT_BLUE} {'─' * 70}{C.RESET}")
702
-
703
- # Save log
704
- save_log(iteration, output, session_id)
705
- iteration_history.append(output)
706
-
707
- # Check exit condition
708
- signal, should_exit = check_exit_condition(output)
709
-
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"""
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
- 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
737
-
738
- # Prepare context for next iteration
739
- context = f"""
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
- # 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)
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
- # 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")
1108
+ print(f" {C.DIM}Session: {session_id}{C.RESET}")
1109
+ print(f" {C.DIM}Logs: {LOG_DIR}{C.RESET}\n")
762
1110
 
763
- return 1
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__":
@@ -13,7 +13,7 @@ long_description = readme_file.read_text(encoding="utf-8") if readme_file.exists
13
13
 
14
14
  setup(
15
15
  name="daveloop",
16
- version="1.2.0",
16
+ version="1.3.0",
17
17
  description="Self-healing debug agent powered by Claude Code CLI",
18
18
  long_description=long_description,
19
19
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes
File without changes
File without changes