daveloop 1.0.0__py3-none-any.whl → 1.1.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.py CHANGED
@@ -1,716 +1,694 @@
1
- #!/usr/bin/env python3
2
- """
3
- DaveLoop - Self-Healing Debug Agent
4
- Orchestrates Claude Code CLI in a feedback loop until bugs are resolved.
5
- """
6
-
7
- import subprocess
8
- import sys
9
- import os
10
- import argparse
11
- import threading
12
- import time
13
- import itertools
14
- from datetime import datetime
15
- from pathlib import Path
16
-
17
- # Configuration
18
- MAX_ITERATIONS = 20
19
-
20
- # Find prompt file - works both when running as script and when installed as package
21
- def find_prompt_file():
22
- """Find the prompt file in the correct location."""
23
- # Try script directory first (for development)
24
- script_dir = Path(__file__).parent
25
- script_prompt = script_dir / "daveloop_prompt.md"
26
- if script_prompt.exists():
27
- return script_prompt
28
-
29
- # Try package resources (for pip install)
30
- try:
31
- import importlib.resources as pkg_resources
32
- try:
33
- # Python 3.9+
34
- files = pkg_resources.files(__package__ or __name__.split('.')[0])
35
- return files / "daveloop_prompt.md"
36
- except AttributeError:
37
- # Python 3.7-3.8
38
- with pkg_resources.path(__package__ or __name__.split('.')[0], "daveloop_prompt.md") as p:
39
- return p
40
- except (ImportError, FileNotFoundError):
41
- pass
42
-
43
- # Fallback to script directory
44
- return script_prompt
45
-
46
- SCRIPT_DIR = Path(__file__).parent
47
- PROMPT_FILE = find_prompt_file()
48
- LOG_DIR = Path.cwd() / "logs" # Use current working directory for logs
49
-
50
- # Exit signals from Claude Code
51
- SIGNAL_RESOLVED = "[DAVELOOP:RESOLVED]"
52
- SIGNAL_BLOCKED = "[DAVELOOP:BLOCKED]"
53
- SIGNAL_CLARIFY = "[DAVELOOP:CLARIFY]"
54
-
55
- # ============================================================================
56
- # ANSI Color Codes
57
- # ============================================================================
58
- class Colors:
59
- RESET = "\033[0m"
60
- BOLD = "\033[1m"
61
- DIM = "\033[2m"
62
-
63
- # Foreground
64
- BLACK = "\033[30m"
65
- RED = "\033[31m"
66
- GREEN = "\033[32m"
67
- YELLOW = "\033[33m"
68
- BLUE = "\033[34m"
69
- MAGENTA = "\033[35m"
70
- CYAN = "\033[36m"
71
- WHITE = "\033[37m"
72
-
73
- # Bright foreground
74
- BRIGHT_RED = "\033[91m"
75
- BRIGHT_GREEN = "\033[92m"
76
- BRIGHT_YELLOW = "\033[93m"
77
- BRIGHT_BLUE = "\033[94m"
78
- BRIGHT_MAGENTA = "\033[95m"
79
- BRIGHT_CYAN = "\033[96m"
80
- BRIGHT_WHITE = "\033[97m"
81
-
82
- # Background
83
- BG_BLACK = "\033[40m"
84
- BG_RED = "\033[41m"
85
- BG_GREEN = "\033[42m"
86
- BG_BLUE = "\033[44m"
87
- BG_MAGENTA = "\033[45m"
88
- BG_CYAN = "\033[46m"
89
-
90
- C = Colors # Shorthand
91
-
92
- # Enable ANSI and UTF-8 on Windows
93
- if sys.platform == "win32":
94
- os.system("chcp 65001 >nul 2>&1") # Set console to UTF-8
95
- os.system("") # Enables ANSI escape sequences in Windows terminal
96
- # Force UTF-8 encoding for stdout/stderr (only if not already wrapped)
97
- import io
98
- if not isinstance(sys.stdout, io.TextIOWrapper) or sys.stdout.encoding != 'utf-8':
99
- if hasattr(sys.stdout, 'buffer'):
100
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
101
- if not isinstance(sys.stderr, io.TextIOWrapper) or sys.stderr.encoding != 'utf-8':
102
- if hasattr(sys.stderr, 'buffer'):
103
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
104
-
105
- # ============================================================================
106
- # ASCII Art Banner
107
- # ============================================================================
108
- BANNER = f"""
109
- {C.BRIGHT_BLUE}{C.BOLD}
110
- ██████╗ █████╗ ██╗ ██╗███████╗██╗ ██████╗ ██████╗ ██████╗
111
- ██╔══██╗██╔══██╗██║ ██║██╔════╝██║ ██╔═══██╗██╔═══██╗██╔══██╗
112
- ██║ ██║███████║██║ ██║█████╗ ██║ ██║ ██║██║ ██║██████╔╝
113
- ██║ ██║██╔══██║╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║ ██║██╔═══╝
114
- ██████╔╝██║ ██║ ╚████╔╝ ███████╗███████╗╚██████╔╝╚██████╔╝██║
115
- ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝
116
- {C.RESET}
117
- {C.BRIGHT_WHITE}{C.BOLD} Self-Healing Debug Agent{C.RESET}
118
- {C.WHITE} Powered by Claude Code - Autonomous Mode{C.RESET}
119
- """
120
-
121
- # ============================================================================
122
- # UI Components
123
- # ============================================================================
124
- def print_header_box(title: str, color: str = C.BRIGHT_BLUE):
125
- """Print a header."""
126
- print(f"\n{color}{C.BOLD}{title}{C.RESET}")
127
- print(f"{color}{'─'*len(title)}{C.RESET}\n")
128
-
129
- def print_section(title: str, color: str = C.BRIGHT_BLUE):
130
- """Print a section divider."""
131
- print(f"\n{color}{C.BOLD}{title}{C.RESET}")
132
- print(f"{color}{'─'*len(title)}{C.RESET}\n")
133
-
134
- def print_status(label: str, value: str, color: str = C.WHITE):
135
- """Print a status line."""
136
- print(f" {C.WHITE}{label}:{C.RESET} {color}{value}{C.RESET}")
137
-
138
- def print_iteration_header(iteration: int, max_iter: int):
139
- """Print the iteration header with visual progress."""
140
- progress = iteration / max_iter
141
- bar_width = 30
142
- filled = int(bar_width * progress)
143
- bar = f"{C.BLUE}{'█' * filled}{C.DIM}{'░' * (bar_width - filled)}{C.RESET}"
144
-
145
- iteration_text = f"ITERATION {iteration}/{max_iter}"
146
- percentage_text = f"{int(progress*100)}%"
147
-
148
- print(f"\n{C.BOLD}{C.WHITE}{iteration_text}{C.RESET} {bar} {C.BRIGHT_BLUE}{percentage_text}{C.RESET}\n")
149
-
150
- def print_success_box(message: str):
151
- """Print an epic success message."""
152
- print(f"\n{C.BRIGHT_GREEN}{C.BOLD}")
153
- print(" ███████╗ ██╗ ██╗ ██████╗ ██████╗ ███████╗ ███████╗ ███████╗")
154
- print(" ██╔════╝ ██║ ██║ ██╔════╝ ██╔════╝ ██╔════╝ ██╔════╝ ██╔════╝")
155
- print(" ███████╗ ██║ ██║ ██║ ██║ █████╗ ███████╗ ███████╗")
156
- print(" ╚════██║ ██║ ██║ ██║ ██║ ██╔══╝ ╚════██║ ╚════██║")
157
- print(" ███████║ ╚██████╔╝ ╚██████╗ ╚██████╗ ███████╗ ███████║ ███████║")
158
- print(" ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚══════╝")
159
- print()
160
- print(f" {C.BRIGHT_YELLOW}★ ★ ★{C.RESET}{C.BRIGHT_GREEN}{C.BOLD} {C.BRIGHT_WHITE}BUG SUCCESSFULLY RESOLVED{C.RESET}{C.BRIGHT_GREEN}{C.BOLD} {C.BRIGHT_YELLOW}★ ★ ★{C.RESET}")
161
- print()
162
- print(f" {C.WHITE}{message}{C.RESET}")
163
- print(f"{C.RESET}\n")
164
-
165
- def print_error_box(message: str):
166
- """Print an error message."""
167
- print(f"\n{C.BRIGHT_RED}{C.BOLD}✗ ERROR: {C.WHITE}{message}{C.RESET}\n")
168
-
169
- def print_warning_box(message: str):
170
- """Print a warning message."""
171
- print(f"\n{C.BRIGHT_YELLOW}{C.BOLD}⚠ WARNING: {C.WHITE}{message}{C.RESET}\n")
172
-
173
- # ============================================================================
174
- # Spinner Animation
175
- # ============================================================================
176
- class Spinner:
177
- """Animated spinner for showing work in progress."""
178
-
179
- def __init__(self, message: str = "Processing"):
180
- self.message = message
181
- self.running = False
182
- self.thread = None
183
- self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
184
- self.start_time = None
185
-
186
- def spin(self):
187
- idx = 0
188
- while self.running:
189
- elapsed = time.time() - self.start_time
190
- frame = self.frames[idx % len(self.frames)]
191
- sys.stdout.write(f"\r {C.BRIGHT_CYAN}{frame}{C.RESET} {C.BOLD}{self.message}{C.RESET} {C.DIM}({elapsed:.0f}s){C.RESET} ")
192
- sys.stdout.flush()
193
- idx += 1
194
- time.sleep(0.1)
195
-
196
- def start(self):
197
- self.running = True
198
- self.start_time = time.time()
199
- self.thread = threading.Thread(target=self.spin)
200
- self.thread.start()
201
-
202
- def stop(self, final_message: str = None):
203
- self.running = False
204
- if self.thread:
205
- self.thread.join()
206
- elapsed = time.time() - self.start_time
207
- if final_message:
208
- sys.stdout.write(f"\r {C.GREEN}✓{C.RESET} {final_message} {C.DIM}({elapsed:.1f}s){C.RESET} \n")
209
- else:
210
- sys.stdout.write(f"\r {C.GREEN}✓{C.RESET} {self.message} complete {C.DIM}({elapsed:.1f}s){C.RESET} \n")
211
- sys.stdout.flush()
212
-
213
- # ============================================================================
214
- # Output Formatter
215
- # ============================================================================
216
- def format_claude_output(output: str) -> str:
217
- """Format Claude's output with colors and sections."""
218
- lines = output.split('\n')
219
- formatted = []
220
- in_reasoning = False
221
- in_code = False
222
-
223
- for line in lines:
224
- # Reasoning block
225
- if "=== DAVELOOP REASONING ===" in line:
226
- in_reasoning = True
227
- formatted.append(f"\n{C.BRIGHT_YELLOW}┌{'─'*50}┐{C.RESET}")
228
- formatted.append(f"{C.BRIGHT_YELLOW}│{C.BOLD} 🧠 REASONING{C.RESET}")
229
- formatted.append(f"{C.BRIGHT_YELLOW}├{'─'*50}┤{C.RESET}")
230
- continue
231
- elif "===========================" in line and in_reasoning:
232
- in_reasoning = False
233
- formatted.append(f"{C.BRIGHT_YELLOW}└{'─'*50}┘{C.RESET}\n")
234
- continue
235
-
236
- # Verification block
237
- if "=== VERIFICATION ===" in line:
238
- formatted.append(f"\n{C.BRIGHT_GREEN}{'─'*50}{C.RESET}")
239
- formatted.append(f"{C.BRIGHT_GREEN}│{C.BOLD} ✓ VERIFICATION{C.RESET}")
240
- formatted.append(f"{C.BRIGHT_GREEN}{'─'*50}{C.RESET}")
241
- continue
242
- elif "====================" in line:
243
- formatted.append(f"{C.BRIGHT_GREEN}└{'─'*50}┘{C.RESET}\n")
244
- continue
245
-
246
- # Code blocks
247
- if line.strip().startswith("```"):
248
- in_code = not in_code
249
- if in_code:
250
- formatted.append(f"{C.DIM}┌─ code ────────────────────────────────{C.RESET}")
251
- else:
252
- formatted.append(f"{C.DIM}└───────────────────────────────────────{C.RESET}")
253
- continue
254
-
255
- # Reasoning labels
256
- if in_reasoning:
257
- if line.startswith("KNOWN:"):
258
- formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.CYAN}KNOWN:{C.RESET}{line[6:]}")
259
- elif line.startswith("UNKNOWN:"):
260
- formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.MAGENTA}UNKNOWN:{C.RESET}{line[8:]}")
261
- elif line.startswith("HYPOTHESIS:"):
262
- formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.YELLOW}HYPOTHESIS:{C.RESET}{line[11:]}")
263
- elif line.startswith("NEXT ACTION:"):
264
- formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.GREEN}NEXT ACTION:{C.RESET}{line[12:]}")
265
- elif line.startswith("WHY:"):
266
- formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.BLUE}WHY:{C.RESET}{line[4:]}")
267
- else:
268
- formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {line}")
269
- continue
270
-
271
- # Exit signals - dim them out, don't make prominent
272
- if "[DAVELOOP:RESOLVED]" in line:
273
- formatted.append(f" {C.DIM}→ [Exit signal: RESOLVED]{C.RESET}")
274
- continue
275
- elif "[DAVELOOP:BLOCKED]" in line:
276
- formatted.append(f" {C.DIM}→ [Exit signal: BLOCKED]{C.RESET}")
277
- continue
278
- elif "[DAVELOOP:CLARIFY]" in line:
279
- formatted.append(f" {C.DIM}→ [Exit signal: CLARIFY]{C.RESET}")
280
- continue
281
-
282
- # Code content
283
- if in_code:
284
- formatted.append(f"{C.DIM}│{C.RESET} {C.WHITE}{line}{C.RESET}")
285
- continue
286
-
287
- # Regular content
288
- formatted.append(f" {line}")
289
-
290
- return '\n'.join(formatted)
291
-
292
- # ============================================================================
293
- # Core Functions
294
- # ============================================================================
295
- def load_prompt() -> str:
296
- """Load the DaveLoop system prompt."""
297
- if PROMPT_FILE.exists():
298
- return PROMPT_FILE.read_text(encoding="utf-8")
299
- else:
300
- print_warning_box(f"Prompt file not found: {PROMPT_FILE}")
301
- return "You are debugging. Fix the bug. Output [DAVELOOP:RESOLVED] when done."
302
-
303
-
304
- def find_claude_cli():
305
- """Find Claude CLI executable path."""
306
- import platform
307
- import shutil
308
-
309
- # 1. Check environment variable (highest priority)
310
- env_path = os.environ.get('CLAUDE_CLI_PATH')
311
- if env_path and os.path.exists(env_path):
312
- return env_path
313
-
314
- # 2. Try common installation paths
315
- is_windows = platform.system() == "Windows"
316
- if is_windows:
317
- common_paths = [
318
- os.path.expanduser("~\\AppData\\Local\\Programs\\claude\\claude.cmd"),
319
- os.path.expanduser("~\\AppData\\Roaming\\npm\\claude.cmd"),
320
- "C:\\Program Files\\Claude\\claude.cmd",
321
- "C:\\Program Files (x86)\\Claude\\claude.cmd",
322
- ]
323
- for path in common_paths:
324
- if os.path.exists(path):
325
- return path
326
- else:
327
- common_paths = [
328
- "/usr/local/bin/claude",
329
- "/usr/bin/claude",
330
- os.path.expanduser("~/.local/bin/claude"),
331
- ]
332
- for path in common_paths:
333
- if os.path.exists(path):
334
- return path
335
-
336
- # 3. Check if it's in PATH
337
- claude_name = "claude.cmd" if is_windows else "claude"
338
- if shutil.which(claude_name):
339
- return claude_name
340
-
341
- # 4. Not found
342
- return None
343
-
344
-
345
- def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool = False, stream: bool = True) -> str:
346
- """Execute Claude Code CLI with the given prompt.
347
-
348
- If stream=True, output is printed in real-time and also returned.
349
- """
350
- claude_cmd = find_claude_cli()
351
- if not claude_cmd:
352
- error_msg = (
353
- "Claude CLI not found!\n\n"
354
- "Please install Claude Code CLI or set CLAUDE_CLI_PATH environment variable:\n"
355
- " Windows: set CLAUDE_CLI_PATH=C:\\path\\to\\claude.cmd\n"
356
- " Linux/Mac: export CLAUDE_CLI_PATH=/path/to/claude\n\n"
357
- "Install from: https://github.com/anthropics/claude-code"
358
- )
359
- print_error_box(error_msg)
360
- return "[DAVELOOP:ERROR] Claude CLI not found"
361
-
362
- cmd = [claude_cmd]
363
-
364
- if continue_session:
365
- cmd.append("--continue")
366
-
367
- cmd.extend(["-p", "--verbose", "--output-format", "stream-json", "--allowedTools", "Bash,Read,Write,Edit,Glob,Grep,Task"])
368
-
369
- try:
370
- if stream:
371
- # Stream output in real-time
372
- process = subprocess.Popen(
373
- cmd,
374
- stdin=subprocess.PIPE,
375
- stdout=subprocess.PIPE,
376
- stderr=subprocess.STDOUT,
377
- text=True,
378
- encoding='utf-8',
379
- errors='replace',
380
- cwd=working_dir,
381
- bufsize=1 # Line buffered
382
- )
383
-
384
- # Send prompt and close stdin
385
- process.stdin.write(prompt)
386
- process.stdin.close()
387
-
388
- # Heartbeat thread to show we're alive
389
- start_time = time.time()
390
- heartbeat_active = True
391
-
392
- def heartbeat():
393
- while heartbeat_active:
394
- elapsed = int(time.time() - start_time)
395
- print(f"\r {C.BLUE}[{elapsed}s elapsed...]{C.RESET} ", end='')
396
- sys.stdout.flush()
397
- time.sleep(3)
398
-
399
- heartbeat_thread = threading.Thread(target=heartbeat, daemon=True)
400
- heartbeat_thread.start()
401
-
402
- # Read and display JSON stream output
403
- import json
404
- output_lines = []
405
- full_text = []
406
-
407
- for line in process.stdout:
408
- # Clear heartbeat line
409
- print(f"\r{' '*40}\r", end='')
410
-
411
- line = line.strip()
412
- if not line:
413
- continue
414
-
415
- try:
416
- data = json.loads(line)
417
- msg_type = data.get("type", "")
418
-
419
- # Handle different message types
420
- if msg_type == "assistant":
421
- # Assistant text message
422
- content = data.get("message", {}).get("content", [])
423
- for block in content:
424
- if block.get("type") == "text":
425
- text = block.get("text", "")
426
- for line_text in text.split('\n'):
427
- formatted = format_output_line(line_text)
428
- print(formatted)
429
- full_text.append(text)
430
-
431
- elif msg_type == "content_block_delta":
432
- # Streaming text delta
433
- delta = data.get("delta", {})
434
- if delta.get("type") == "text_delta":
435
- text = delta.get("text", "")
436
- print(text, end='')
437
- full_text.append(text)
438
-
439
- elif msg_type == "tool_use":
440
- # Tool being used
441
- tool_name = data.get("name", "unknown")
442
- print(f"\n {C.BLUE}🔧 Using tool: {tool_name}{C.RESET}")
443
-
444
- elif msg_type == "tool_result":
445
- # Tool result
446
- print(f" {C.BLUE}✓ Tool completed{C.RESET}\n")
447
-
448
- elif msg_type == "result":
449
- # Final result
450
- text = data.get("result", "")
451
- if text:
452
- for line_text in text.split('\n'):
453
- formatted = format_output_line(line_text)
454
- print(formatted)
455
- full_text.append(text)
456
-
457
- elif msg_type == "error":
458
- error_msg = data.get("error", {}).get("message", "Unknown error")
459
- print(f" {C.RED}ERROR: {error_msg}{C.RESET}")
460
-
461
- sys.stdout.flush()
462
-
463
- except json.JSONDecodeError:
464
- # Not JSON, just print as-is
465
- print(f" {line}")
466
- full_text.append(line)
467
-
468
- output_lines.append(line)
469
-
470
- heartbeat_active = False
471
- print(f"\r{' '*40}\r", end='') # Clear final heartbeat
472
-
473
- process.wait(timeout=600)
474
- return '\n'.join(full_text)
475
- else:
476
- # Non-streaming mode
477
- result = subprocess.run(
478
- cmd,
479
- input=prompt,
480
- capture_output=True,
481
- text=True,
482
- encoding='utf-8',
483
- errors='replace',
484
- cwd=working_dir,
485
- timeout=600
486
- )
487
- output = result.stdout
488
- if result.stderr:
489
- output += f"\n{C.RED}[STDERR]{C.RESET}\n{result.stderr}"
490
- return output
491
-
492
- except subprocess.TimeoutExpired:
493
- return "[DAVELOOP:TIMEOUT] Claude Code iteration timed out after 10 minutes"
494
- except FileNotFoundError:
495
- return "[DAVELOOP:ERROR] Claude Code CLI not found. Is it installed?"
496
- except Exception as e:
497
- return f"[DAVELOOP:ERROR] {str(e)}"
498
-
499
-
500
- def format_output_line(line: str) -> str:
501
- """Format a single line of Claude's output with colors."""
502
- # Reasoning markers
503
- if "=== DAVELOOP REASONING ===" in line:
504
- return f"\n{C.BRIGHT_BLUE}{'─'*50}\n 🧠 REASONING\n{'─'*50}{C.RESET}"
505
- if "===========================" in line:
506
- return f"{C.BRIGHT_BLUE}{'─'*50}{C.RESET}\n"
507
-
508
- # Reasoning labels
509
- if line.startswith("KNOWN:"):
510
- return f" {C.BLUE}KNOWN:{C.RESET}{C.WHITE}{line[6:]}{C.RESET}"
511
- if line.startswith("UNKNOWN:"):
512
- return f" {C.BLUE}UNKNOWN:{C.RESET}{C.WHITE}{line[8:]}{C.RESET}"
513
- if line.startswith("HYPOTHESIS:"):
514
- return f" {C.BLUE}HYPOTHESIS:{C.RESET}{C.WHITE}{line[11:]}{C.RESET}"
515
- if line.startswith("NEXT ACTION:"):
516
- return f" {C.BLUE}NEXT ACTION:{C.RESET}{C.WHITE}{line[12:]}{C.RESET}"
517
- if line.startswith("WHY:"):
518
- return f" {C.BLUE}WHY:{C.RESET}{C.WHITE}{line[4:]}{C.RESET}"
519
-
520
- # Exit signals - just dim them out in the stream, don't make them prominent
521
- # The actual success/error boxes will be shown after iteration completes
522
- if "[DAVELOOP:RESOLVED]" in line:
523
- return f" {C.DIM}→ [Exit signal detected: RESOLVED]{C.RESET}"
524
- if "[DAVELOOP:BLOCKED]" in line:
525
- return f" {C.DIM}→ [Exit signal detected: BLOCKED]{C.RESET}"
526
- if "[DAVELOOP:CLARIFY]" in line:
527
- return f" {C.DIM}→ [Exit signal detected: CLARIFY]{C.RESET}"
528
-
529
- # Code blocks
530
- if line.strip().startswith("```"):
531
- return f"{C.BLUE}{'─'*40}{C.RESET}"
532
-
533
- # Default - white text
534
- return f" {C.WHITE}{line}{C.RESET}"
535
-
536
-
537
- def check_exit_condition(output: str) -> tuple[str, bool]:
538
- """Check if we should exit the loop."""
539
- if SIGNAL_RESOLVED in output:
540
- return "RESOLVED", True
541
- if SIGNAL_BLOCKED in output:
542
- return "BLOCKED", True
543
- if SIGNAL_CLARIFY in output:
544
- return "CLARIFY", True
545
- if "[DAVELOOP:ERROR]" in output:
546
- return "ERROR", True
547
- if "[DAVELOOP:TIMEOUT]" in output:
548
- return "TIMEOUT", False
549
- return "CONTINUE", False
550
-
551
-
552
- def save_log(iteration: int, content: str, session_id: str):
553
- """Save iteration log to file."""
554
- LOG_DIR.mkdir(exist_ok=True)
555
- log_file = LOG_DIR / f"{session_id}_iteration_{iteration:02d}.log"
556
- log_file.write_text(content, encoding="utf-8")
557
-
558
-
559
- # ============================================================================
560
- # Main Entry Point
561
- # ============================================================================
562
- def main():
563
- parser = argparse.ArgumentParser(
564
- description="DaveLoop - Self-Healing Debug Agent",
565
- formatter_class=argparse.RawDescriptionHelpFormatter
566
- )
567
- parser.add_argument("bug", nargs="?", help="Bug description or error message")
568
- parser.add_argument("-f", "--file", help="Read bug description from file")
569
- parser.add_argument("-d", "--dir", help="Working directory for Claude Code")
570
- parser.add_argument("-m", "--max-iterations", type=int, default=MAX_ITERATIONS)
571
- parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
572
-
573
- args = parser.parse_args()
574
-
575
- # Clear screen and show banner
576
- os.system('cls' if os.name == 'nt' else 'clear')
577
- print(BANNER)
578
-
579
- # Get bug description
580
- if args.file:
581
- bug_input = Path(args.file).read_text(encoding="utf-8")
582
- elif args.bug:
583
- bug_input = args.bug
584
- else:
585
- print(f" {C.CYAN}Describe the bug (Ctrl+D or Ctrl+Z to finish):{C.RESET}")
586
- bug_input = sys.stdin.read().strip()
587
-
588
- if not bug_input:
589
- print_error_box("No bug description provided")
590
- return 1
591
-
592
- # Setup
593
- session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
594
- system_prompt = load_prompt()
595
- working_dir = args.dir or os.getcwd()
596
-
597
- # Session info
598
- print_header_box(f"SESSION: {session_id}", C.BRIGHT_BLUE)
599
- print_status("Working Directory", working_dir, C.WHITE)
600
- print_status("Max Iterations", str(args.max_iterations), C.WHITE)
601
- print_status("Context Mode", "PERSISTENT (--continue)", C.WHITE)
602
- print_status("System Prompt", f"{len(system_prompt)} chars loaded", C.WHITE)
603
- print()
604
-
605
- print_section("BUG REPORT", C.BRIGHT_RED)
606
- # Wrap bug input nicely
607
- for line in bug_input.split('\n')[:10]:
608
- print(f" {C.RED}{line[:80]}{C.RESET}")
609
- if len(bug_input.split('\n')) > 10:
610
- print(f" {C.RED}... ({len(bug_input.split(chr(10))) - 10} more lines){C.RESET}")
611
- print()
612
-
613
- sys.stdout.flush()
614
-
615
- # Initial context
616
- context = f"""
617
- ## Bug Report
618
-
619
- {bug_input}
620
-
621
- ## Instructions
622
-
623
- Analyze this bug. Gather whatever logs/information you need to understand it.
624
- Then fix it. Use the reasoning protocol before each action.
625
- """
626
-
627
- iteration_history = []
628
-
629
- for iteration in range(1, args.max_iterations + 1):
630
- print_iteration_header(iteration, args.max_iterations)
631
-
632
- if iteration == 1:
633
- full_prompt = f"{system_prompt}\n\n---\n\n{context}"
634
- continue_session = False
635
- else:
636
- full_prompt = context
637
- continue_session = True
638
-
639
- if args.verbose:
640
- print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
641
-
642
- # Show "Claude is working" indicator
643
- print(f"\n {C.BRIGHT_BLUE}▶ Claude is working...{C.RESET}\n")
644
- sys.stdout.flush()
645
-
646
- # Run Claude with real-time streaming output
647
- output = run_claude_code(full_prompt, working_dir, continue_session=continue_session, stream=True)
648
-
649
- print(f"\n {C.BLUE}✓ Iteration complete{C.RESET}\n")
650
-
651
- # Save log
652
- save_log(iteration, output, session_id)
653
- iteration_history.append(output)
654
-
655
- # Check exit condition
656
- signal, should_exit = check_exit_condition(output)
657
-
658
- if should_exit:
659
- if signal == "RESOLVED":
660
- print_success_box(f"Bug fixed in {iteration} iteration(s)!")
661
- print_status("Session", session_id, C.WHITE)
662
- print_status("Logs", str(LOG_DIR), C.WHITE)
663
- print()
664
- return 0
665
- elif signal == "CLARIFY":
666
- print_warning_box("Claude needs clarification")
667
- print(f"\n {C.BLUE}Your response:{C.RESET}")
668
- human_input = input(f" {C.WHITE}> {C.RESET}")
669
- context = f"""
670
- ## Human Clarification
671
-
672
- {human_input}
673
-
674
- Continue debugging with this information. Use the reasoning protocol before each action.
675
- """
676
- continue
677
- elif signal == "BLOCKED":
678
- print_error_box(f"Claude is blocked - needs human help")
679
- print_status("Session", session_id, C.WHITE)
680
- print_status("Logs", str(LOG_DIR), C.WHITE)
681
- print()
682
- return 1
683
- else:
684
- print_error_box(f"Error occurred: {signal}")
685
- return 1
686
-
687
- # Prepare context for next iteration
688
- context = f"""
689
- ## Iteration {iteration + 1}
690
-
691
- The bug is NOT yet resolved. You have full context from previous iterations.
692
-
693
- Continue debugging. Analyze what happened, determine next steps, and proceed.
694
- Use the reasoning protocol before each action.
695
- """
696
-
697
- # Max iterations reached
698
- print_warning_box(f"Max iterations ({args.max_iterations}) reached")
699
- print_status("Session", session_id, C.WHITE)
700
- print_status("Logs", str(LOG_DIR), C.WHITE)
701
- print()
702
-
703
- # Save summary
704
- summary = f"# DaveLoop Session {session_id}\n\n"
705
- summary += f"Bug: {bug_input[:200]}...\n\n"
706
- summary += f"Iterations: {args.max_iterations}\n\n"
707
- summary += "## Iteration History\n\n"
708
- for i, hist in enumerate(iteration_history, 1):
709
- summary += f"### Iteration {i}\n```\n{hist[:500]}...\n```\n\n"
710
- (LOG_DIR / f"{session_id}_summary.md").write_text(summary, encoding="utf-8")
711
-
712
- return 1
713
-
714
-
715
- if __name__ == "__main__":
716
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ """
3
+ DaveLoop - Self-Healing Debug Agent
4
+ Orchestrates Claude Code CLI in a feedback loop until bugs are resolved.
5
+ """
6
+
7
+ import subprocess
8
+ import sys
9
+ import os
10
+ import argparse
11
+ import threading
12
+ import time
13
+ import itertools
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+
17
+ # Configuration
18
+ MAX_ITERATIONS = 20
19
+ DEFAULT_TIMEOUT = 600 # 10 minutes in seconds
20
+ SCRIPT_DIR = Path(__file__).parent
21
+ PROMPT_FILE = SCRIPT_DIR / "daveloop_prompt.md"
22
+ LOG_DIR = SCRIPT_DIR / "logs"
23
+
24
+ # Exit signals from Claude Code
25
+ SIGNAL_RESOLVED = "[DAVELOOP:RESOLVED]"
26
+ SIGNAL_BLOCKED = "[DAVELOOP:BLOCKED]"
27
+ SIGNAL_CLARIFY = "[DAVELOOP:CLARIFY]"
28
+
29
+ # ============================================================================
30
+ # ANSI Color Codes
31
+ # ============================================================================
32
+ class Colors:
33
+ RESET = "\033[0m"
34
+ BOLD = "\033[1m"
35
+ DIM = "\033[2m"
36
+
37
+ # Foreground
38
+ BLACK = "\033[30m"
39
+ RED = "\033[31m"
40
+ GREEN = "\033[32m"
41
+ YELLOW = "\033[33m"
42
+ BLUE = "\033[34m"
43
+ MAGENTA = "\033[35m"
44
+ CYAN = "\033[36m"
45
+ WHITE = "\033[37m"
46
+
47
+ # Bright foreground
48
+ BRIGHT_RED = "\033[91m"
49
+ BRIGHT_GREEN = "\033[92m"
50
+ BRIGHT_YELLOW = "\033[93m"
51
+ BRIGHT_BLUE = "\033[94m"
52
+ BRIGHT_MAGENTA = "\033[95m"
53
+ BRIGHT_CYAN = "\033[96m"
54
+ BRIGHT_WHITE = "\033[97m"
55
+
56
+ # Background
57
+ BG_BLACK = "\033[40m"
58
+ BG_RED = "\033[41m"
59
+ BG_GREEN = "\033[42m"
60
+ BG_BLUE = "\033[44m"
61
+ BG_MAGENTA = "\033[45m"
62
+ BG_CYAN = "\033[46m"
63
+
64
+ C = Colors # Shorthand
65
+
66
+ # Enable ANSI and UTF-8 on Windows
67
+ if sys.platform == "win32":
68
+ os.system("chcp 65001 >nul 2>&1") # Set console to UTF-8
69
+ os.system("") # Enables ANSI escape sequences in Windows terminal
70
+ # Force UTF-8 encoding for stdout/stderr (only if not already wrapped)
71
+ import io
72
+ if not isinstance(sys.stdout, io.TextIOWrapper) or sys.stdout.encoding != 'utf-8':
73
+ if hasattr(sys.stdout, 'buffer'):
74
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
75
+ if not isinstance(sys.stderr, io.TextIOWrapper) or sys.stderr.encoding != 'utf-8':
76
+ if hasattr(sys.stderr, 'buffer'):
77
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
78
+
79
+ # ============================================================================
80
+ # ASCII Art Banner
81
+ # ============================================================================
82
+ BANNER = f"""
83
+ {C.BRIGHT_BLUE}{C.BOLD}
84
+ ██████╗ █████╗ ██╗ ██╗███████╗██╗ ██████╗ ██████╗ ██████╗
85
+ ██╔══██╗██╔══██╗██║ ██║██╔════╝██║ ██╔═══██╗██╔═══██╗██╔══██╗
86
+ ██║ ██║███████║██║ ██║█████╗ ██║ ██║ ██║██║ ██║██████╔╝
87
+ ██║ ██║██╔══██║╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║ ██║██╔═══╝
88
+ ██████╔╝██║ ██║ ╚████╔╝ ███████╗███████╗╚██████╔╝╚██████╔╝██║
89
+ ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝
90
+ {C.RESET}
91
+ {C.BRIGHT_WHITE}{C.BOLD} Self-Healing Debug Agent{C.RESET}
92
+ {C.WHITE} Powered by Claude Code - Autonomous Mode{C.RESET}
93
+ """
94
+
95
+ # ============================================================================
96
+ # UI Components
97
+ # ============================================================================
98
+ def print_header_box(title: str, color: str = C.BRIGHT_BLUE):
99
+ """Print a header."""
100
+ print(f"\n{color}{C.BOLD}{title}{C.RESET}")
101
+ print(f"{color}{'─'*len(title)}{C.RESET}\n")
102
+
103
+ def print_section(title: str, color: str = C.BRIGHT_BLUE):
104
+ """Print a section divider."""
105
+ print(f"\n{color}{C.BOLD}{title}{C.RESET}")
106
+ print(f"{color}{'─'*len(title)}{C.RESET}\n")
107
+
108
+ def print_status(label: str, value: str, color: str = C.WHITE):
109
+ """Print a status line."""
110
+ print(f" {C.WHITE}{label}:{C.RESET} {color}{value}{C.RESET}")
111
+
112
+ def print_iteration_header(iteration: int, max_iter: int):
113
+ """Print the iteration header with visual progress."""
114
+ progress = iteration / max_iter
115
+ bar_width = 30
116
+ filled = int(bar_width * progress)
117
+ bar = f"{C.BLUE}{'█' * filled}{C.DIM}{'░' * (bar_width - filled)}{C.RESET}"
118
+
119
+ iteration_text = f"ITERATION {iteration}/{max_iter}"
120
+ percentage_text = f"{int(progress*100)}%"
121
+
122
+ print(f"\n{C.BOLD}{C.WHITE}{iteration_text}{C.RESET} {bar} {C.BRIGHT_BLUE}{percentage_text}{C.RESET}\n")
123
+
124
+ def print_success_box(message: str):
125
+ """Print an epic success message."""
126
+ print(f"\n{C.BRIGHT_GREEN}{C.BOLD}")
127
+ print(" ███████╗ ██╗ ██╗ ██████╗ ██████╗ ███████╗ ███████╗ ███████╗")
128
+ print(" ██╔════╝ ██║ ██║ ██╔════╝ ██╔════╝ ██╔════╝ ██╔════╝ ██╔════╝")
129
+ print(" ███████╗ ██║ ██║ ██║ ██║ █████╗ ███████╗ ███████╗")
130
+ print(" ╚════██║ ██║ ██║ ██║ ██║ ██╔══╝ ╚════██║ ╚════██║")
131
+ print(" ███████║ ╚██████╔╝ ╚██████╗ ╚██████╗ ███████╗ ███████║ ███████║")
132
+ print(" ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚══════╝")
133
+ print()
134
+ print(f" {C.BRIGHT_YELLOW}★ ★{C.RESET}{C.BRIGHT_GREEN}{C.BOLD} {C.BRIGHT_WHITE}BUG SUCCESSFULLY RESOLVED{C.RESET}{C.BRIGHT_GREEN}{C.BOLD} {C.BRIGHT_YELLOW}★ ★{C.RESET}")
135
+ print()
136
+ print(f" {C.WHITE}{message}{C.RESET}")
137
+ print(f"{C.RESET}\n")
138
+
139
+ def print_error_box(message: str):
140
+ """Print an error message."""
141
+ print(f"\n{C.BRIGHT_RED}{C.BOLD}✗ ERROR: {C.WHITE}{message}{C.RESET}\n")
142
+
143
+ def print_warning_box(message: str):
144
+ """Print a warning message."""
145
+ print(f"\n{C.BRIGHT_YELLOW}{C.BOLD}⚠ WARNING: {C.WHITE}{message}{C.RESET}\n")
146
+
147
+ # ============================================================================
148
+ # Spinner Animation
149
+ # ============================================================================
150
+ class Spinner:
151
+ """Animated spinner for showing work in progress."""
152
+
153
+ def __init__(self, message: str = "Processing"):
154
+ self.message = message
155
+ self.running = False
156
+ self.thread = None
157
+ self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
158
+ self.start_time = None
159
+
160
+ def spin(self):
161
+ idx = 0
162
+ while self.running:
163
+ elapsed = time.time() - self.start_time
164
+ frame = self.frames[idx % len(self.frames)]
165
+ sys.stdout.write(f"\r {C.BRIGHT_CYAN}{frame}{C.RESET} {C.BOLD}{self.message}{C.RESET} {C.DIM}({elapsed:.0f}s){C.RESET} ")
166
+ sys.stdout.flush()
167
+ idx += 1
168
+ time.sleep(0.1)
169
+
170
+ def start(self):
171
+ self.running = True
172
+ self.start_time = time.time()
173
+ self.thread = threading.Thread(target=self.spin)
174
+ self.thread.start()
175
+
176
+ def stop(self, final_message: str = None):
177
+ self.running = False
178
+ if self.thread:
179
+ self.thread.join()
180
+ elapsed = time.time() - self.start_time
181
+ if final_message:
182
+ sys.stdout.write(f"\r {C.GREEN}✓{C.RESET} {final_message} {C.DIM}({elapsed:.1f}s){C.RESET} \n")
183
+ else:
184
+ sys.stdout.write(f"\r {C.GREEN}✓{C.RESET} {self.message} complete {C.DIM}({elapsed:.1f}s){C.RESET} \n")
185
+ sys.stdout.flush()
186
+
187
+ # ============================================================================
188
+ # Output Formatter
189
+ # ============================================================================
190
+ def format_claude_output(output: str) -> str:
191
+ """Format Claude's output with colors and sections."""
192
+ lines = output.split('\n')
193
+ formatted = []
194
+ in_reasoning = False
195
+ in_code = False
196
+
197
+ for line in lines:
198
+ # Reasoning block
199
+ if "=== DAVELOOP REASONING ===" in line:
200
+ in_reasoning = True
201
+ formatted.append(f"\n{C.BRIGHT_YELLOW}┌{'─'*50}┐{C.RESET}")
202
+ formatted.append(f"{C.BRIGHT_YELLOW}│{C.BOLD} 🧠 REASONING{C.RESET}")
203
+ formatted.append(f"{C.BRIGHT_YELLOW}├{'─'*50}┤{C.RESET}")
204
+ continue
205
+ elif "===========================" in line and in_reasoning:
206
+ in_reasoning = False
207
+ formatted.append(f"{C.BRIGHT_YELLOW}└{'─'*50}┘{C.RESET}\n")
208
+ continue
209
+
210
+ # Verification block
211
+ if "=== VERIFICATION ===" in line:
212
+ formatted.append(f"\n{C.BRIGHT_GREEN}┌{'─'*50}┐{C.RESET}")
213
+ formatted.append(f"{C.BRIGHT_GREEN}│{C.BOLD} ✓ VERIFICATION{C.RESET}")
214
+ formatted.append(f"{C.BRIGHT_GREEN}├{'─'*50}┤{C.RESET}")
215
+ continue
216
+ elif "====================" in line:
217
+ formatted.append(f"{C.BRIGHT_GREEN}└{'─'*50}┘{C.RESET}\n")
218
+ continue
219
+
220
+ # Code blocks
221
+ if line.strip().startswith("```"):
222
+ in_code = not in_code
223
+ if in_code:
224
+ formatted.append(f"{C.DIM}┌─ code ────────────────────────────────{C.RESET}")
225
+ else:
226
+ formatted.append(f"{C.DIM}└───────────────────────────────────────{C.RESET}")
227
+ continue
228
+
229
+ # Reasoning labels
230
+ if in_reasoning:
231
+ if line.startswith("KNOWN:"):
232
+ formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.CYAN}KNOWN:{C.RESET}{line[6:]}")
233
+ elif line.startswith("UNKNOWN:"):
234
+ formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.MAGENTA}UNKNOWN:{C.RESET}{line[8:]}")
235
+ elif line.startswith("HYPOTHESIS:"):
236
+ formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {C.YELLOW}HYPOTHESIS:{C.RESET}{line[11:]}")
237
+ elif line.startswith("NEXT ACTION:"):
238
+ formatted.append(f"{C.BRIGHT_YELLOW}{C.RESET} {C.GREEN}NEXT ACTION:{C.RESET}{line[12:]}")
239
+ elif line.startswith("WHY:"):
240
+ formatted.append(f"{C.BRIGHT_YELLOW}{C.RESET} {C.BLUE}WHY:{C.RESET}{line[4:]}")
241
+ else:
242
+ formatted.append(f"{C.BRIGHT_YELLOW}│{C.RESET} {line}")
243
+ continue
244
+
245
+ # Exit signals - dim them out, don't make prominent
246
+ if "[DAVELOOP:RESOLVED]" in line:
247
+ formatted.append(f" {C.DIM}→ [Exit signal: RESOLVED]{C.RESET}")
248
+ continue
249
+ elif "[DAVELOOP:BLOCKED]" in line:
250
+ formatted.append(f" {C.DIM} [Exit signal: BLOCKED]{C.RESET}")
251
+ continue
252
+ elif "[DAVELOOP:CLARIFY]" in line:
253
+ formatted.append(f" {C.DIM}→ [Exit signal: CLARIFY]{C.RESET}")
254
+ continue
255
+
256
+ # Code content
257
+ if in_code:
258
+ formatted.append(f"{C.DIM}│{C.RESET} {C.WHITE}{line}{C.RESET}")
259
+ continue
260
+
261
+ # Regular content
262
+ formatted.append(f" {line}")
263
+
264
+ return '\n'.join(formatted)
265
+
266
+ # ============================================================================
267
+ # Core Functions
268
+ # ============================================================================
269
+ def load_prompt() -> str:
270
+ """Load the DaveLoop system prompt."""
271
+ if PROMPT_FILE.exists():
272
+ return PROMPT_FILE.read_text(encoding="utf-8")
273
+ else:
274
+ print_warning_box(f"Prompt file not found: {PROMPT_FILE}")
275
+ return "You are debugging. Fix the bug. Output [DAVELOOP:RESOLVED] when done."
276
+
277
+
278
+ def find_claude_cli():
279
+ """Find Claude CLI executable path."""
280
+ import platform
281
+ import shutil
282
+
283
+ # 1. Check environment variable (highest priority)
284
+ env_path = os.environ.get('CLAUDE_CLI_PATH')
285
+ if env_path and os.path.exists(env_path):
286
+ return env_path
287
+
288
+ # 2. Try common installation paths
289
+ is_windows = platform.system() == "Windows"
290
+ if is_windows:
291
+ common_paths = [
292
+ os.path.expanduser("~\\AppData\\Local\\Programs\\claude\\claude.cmd"),
293
+ os.path.expanduser("~\\AppData\\Roaming\\npm\\claude.cmd"),
294
+ "C:\\Program Files\\Claude\\claude.cmd",
295
+ "C:\\Program Files (x86)\\Claude\\claude.cmd",
296
+ ]
297
+ for path in common_paths:
298
+ if os.path.exists(path):
299
+ return path
300
+ else:
301
+ common_paths = [
302
+ "/usr/local/bin/claude",
303
+ "/usr/bin/claude",
304
+ os.path.expanduser("~/.local/bin/claude"),
305
+ ]
306
+ for path in common_paths:
307
+ if os.path.exists(path):
308
+ return path
309
+
310
+ # 3. Check if it's in PATH
311
+ claude_name = "claude.cmd" if is_windows else "claude"
312
+ if shutil.which(claude_name):
313
+ return claude_name
314
+
315
+ # 4. Not found
316
+ return None
317
+
318
+
319
+ def run_claude_code(prompt: str, working_dir: str = None, continue_session: bool = False, stream: bool = True, timeout: int = DEFAULT_TIMEOUT) -> str:
320
+ """Execute Claude Code CLI with the given prompt.
321
+
322
+ If stream=True, output is printed in real-time and also returned.
323
+ timeout is in seconds (default 600 = 10 minutes).
324
+ """
325
+ claude_cmd = find_claude_cli()
326
+ if not claude_cmd:
327
+ error_msg = (
328
+ "Claude CLI not found!\n\n"
329
+ "Please install Claude Code CLI or set CLAUDE_CLI_PATH environment variable:\n"
330
+ " Windows: set CLAUDE_CLI_PATH=C:\\path\\to\\claude.cmd\n"
331
+ " Linux/Mac: export CLAUDE_CLI_PATH=/path/to/claude\n\n"
332
+ "Install from: https://github.com/anthropics/claude-code"
333
+ )
334
+ print_error_box(error_msg)
335
+ return "[DAVELOOP:ERROR] Claude CLI not found"
336
+
337
+ cmd = [claude_cmd]
338
+
339
+ if continue_session:
340
+ cmd.append("--continue")
341
+
342
+ cmd.extend(["-p", "--verbose", "--output-format", "stream-json", "--allowedTools", "Bash,Read,Write,Edit,Glob,Grep,Task"])
343
+
344
+ try:
345
+ if stream:
346
+ # Stream output in real-time
347
+ process = subprocess.Popen(
348
+ cmd,
349
+ stdin=subprocess.PIPE,
350
+ stdout=subprocess.PIPE,
351
+ stderr=subprocess.STDOUT,
352
+ text=True,
353
+ encoding='utf-8',
354
+ errors='replace',
355
+ cwd=working_dir,
356
+ bufsize=1 # Line buffered
357
+ )
358
+
359
+ # Send prompt and close stdin
360
+ process.stdin.write(prompt)
361
+ process.stdin.close()
362
+
363
+ # Heartbeat thread to show we're alive
364
+ start_time = time.time()
365
+ heartbeat_active = True
366
+
367
+ def heartbeat():
368
+ while heartbeat_active:
369
+ elapsed = int(time.time() - start_time)
370
+ print(f"\r {C.BLUE}[{elapsed}s elapsed...]{C.RESET} ", end='')
371
+ sys.stdout.flush()
372
+ time.sleep(3)
373
+
374
+ heartbeat_thread = threading.Thread(target=heartbeat, daemon=True)
375
+ heartbeat_thread.start()
376
+
377
+ # Read and display JSON stream output
378
+ import json
379
+ output_lines = []
380
+ full_text = []
381
+
382
+ for line in process.stdout:
383
+ # Clear heartbeat line
384
+ print(f"\r{' '*40}\r", end='')
385
+
386
+ line = line.strip()
387
+ if not line:
388
+ continue
389
+
390
+ try:
391
+ data = json.loads(line)
392
+ msg_type = data.get("type", "")
393
+
394
+ # Handle different message types
395
+ if msg_type == "assistant":
396
+ # Assistant text message
397
+ content = data.get("message", {}).get("content", [])
398
+ for block in content:
399
+ if block.get("type") == "text":
400
+ text = block.get("text", "")
401
+ for line_text in text.split('\n'):
402
+ formatted = format_output_line(line_text)
403
+ print(formatted)
404
+ full_text.append(text)
405
+
406
+ elif msg_type == "content_block_delta":
407
+ # Streaming text delta
408
+ delta = data.get("delta", {})
409
+ if delta.get("type") == "text_delta":
410
+ text = delta.get("text", "")
411
+ print(text, end='')
412
+ full_text.append(text)
413
+
414
+ elif msg_type == "tool_use":
415
+ # Tool being used
416
+ tool_name = data.get("name", "unknown")
417
+ print(f"\n {C.BLUE}🔧 Using tool: {tool_name}{C.RESET}")
418
+
419
+ elif msg_type == "tool_result":
420
+ # Tool result
421
+ print(f" {C.BLUE}✓ Tool completed{C.RESET}\n")
422
+
423
+ elif msg_type == "result":
424
+ # Final result
425
+ text = data.get("result", "")
426
+ if text:
427
+ for line_text in text.split('\n'):
428
+ formatted = format_output_line(line_text)
429
+ print(formatted)
430
+ full_text.append(text)
431
+
432
+ elif msg_type == "error":
433
+ error_msg = data.get("error", {}).get("message", "Unknown error")
434
+ print(f" {C.RED}ERROR: {error_msg}{C.RESET}")
435
+
436
+ sys.stdout.flush()
437
+
438
+ except json.JSONDecodeError:
439
+ # Not JSON, just print as-is
440
+ print(f" {line}")
441
+ full_text.append(line)
442
+
443
+ output_lines.append(line)
444
+
445
+ heartbeat_active = False
446
+ print(f"\r{' '*40}\r", end='') # Clear final heartbeat
447
+
448
+ process.wait(timeout=timeout)
449
+ return '\n'.join(full_text)
450
+ else:
451
+ # Non-streaming mode
452
+ result = subprocess.run(
453
+ cmd,
454
+ input=prompt,
455
+ capture_output=True,
456
+ text=True,
457
+ encoding='utf-8',
458
+ errors='replace',
459
+ cwd=working_dir,
460
+ timeout=timeout
461
+ )
462
+ output = result.stdout
463
+ if result.stderr:
464
+ output += f"\n{C.RED}[STDERR]{C.RESET}\n{result.stderr}"
465
+ return output
466
+
467
+ except subprocess.TimeoutExpired:
468
+ return f"[DAVELOOP:TIMEOUT] Claude Code iteration timed out after {timeout // 60} minutes"
469
+ except FileNotFoundError:
470
+ return "[DAVELOOP:ERROR] Claude Code CLI not found. Is it installed?"
471
+ except Exception as e:
472
+ return f"[DAVELOOP:ERROR] {str(e)}"
473
+
474
+
475
+ def format_output_line(line: str) -> str:
476
+ """Format a single line of Claude's output with colors."""
477
+ # Reasoning markers
478
+ if "=== DAVELOOP REASONING ===" in line:
479
+ return f"\n{C.BRIGHT_BLUE}{'─'*50}\n 🧠 REASONING\n{'─'*50}{C.RESET}"
480
+ if "===========================" in line:
481
+ return f"{C.BRIGHT_BLUE}{'─'*50}{C.RESET}\n"
482
+
483
+ # Reasoning labels
484
+ if line.startswith("KNOWN:"):
485
+ return f" {C.BLUE}KNOWN:{C.RESET}{C.WHITE}{line[6:]}{C.RESET}"
486
+ if line.startswith("UNKNOWN:"):
487
+ return f" {C.BLUE}UNKNOWN:{C.RESET}{C.WHITE}{line[8:]}{C.RESET}"
488
+ if line.startswith("HYPOTHESIS:"):
489
+ return f" {C.BLUE}HYPOTHESIS:{C.RESET}{C.WHITE}{line[11:]}{C.RESET}"
490
+ if line.startswith("NEXT ACTION:"):
491
+ return f" {C.BLUE}NEXT ACTION:{C.RESET}{C.WHITE}{line[12:]}{C.RESET}"
492
+ if line.startswith("WHY:"):
493
+ return f" {C.BLUE}WHY:{C.RESET}{C.WHITE}{line[4:]}{C.RESET}"
494
+
495
+ # Exit signals - just dim them out in the stream, don't make them prominent
496
+ # The actual success/error boxes will be shown after iteration completes
497
+ if "[DAVELOOP:RESOLVED]" in line:
498
+ return f" {C.DIM}→ [Exit signal detected: RESOLVED]{C.RESET}"
499
+ if "[DAVELOOP:BLOCKED]" in line:
500
+ return f" {C.DIM}→ [Exit signal detected: BLOCKED]{C.RESET}"
501
+ if "[DAVELOOP:CLARIFY]" in line:
502
+ return f" {C.DIM}→ [Exit signal detected: CLARIFY]{C.RESET}"
503
+
504
+ # Code blocks
505
+ if line.strip().startswith("```"):
506
+ return f"{C.BLUE}{'─'*40}{C.RESET}"
507
+
508
+ # Default - white text
509
+ return f" {C.WHITE}{line}{C.RESET}"
510
+
511
+
512
+ def check_exit_condition(output: str) -> tuple[str, bool]:
513
+ """Check if we should exit the loop."""
514
+ if SIGNAL_RESOLVED in output:
515
+ return "RESOLVED", True
516
+ if SIGNAL_BLOCKED in output:
517
+ return "BLOCKED", True
518
+ if SIGNAL_CLARIFY in output:
519
+ return "CLARIFY", True
520
+ if "[DAVELOOP:ERROR]" in output:
521
+ return "ERROR", True
522
+ if "[DAVELOOP:TIMEOUT]" in output:
523
+ return "TIMEOUT", False
524
+ return "CONTINUE", False
525
+
526
+
527
+ def save_log(iteration: int, content: str, session_id: str):
528
+ """Save iteration log to file."""
529
+ LOG_DIR.mkdir(exist_ok=True)
530
+ log_file = LOG_DIR / f"{session_id}_iteration_{iteration:02d}.log"
531
+ log_file.write_text(content, encoding="utf-8")
532
+
533
+
534
+ # ============================================================================
535
+ # Main Entry Point
536
+ # ============================================================================
537
+ def main():
538
+ parser = argparse.ArgumentParser(
539
+ description="DaveLoop - Self-Healing Debug Agent",
540
+ formatter_class=argparse.RawDescriptionHelpFormatter
541
+ )
542
+ parser.add_argument("bug", nargs="?", help="Bug description or error message")
543
+ parser.add_argument("-f", "--file", help="Read bug description from file")
544
+ parser.add_argument("-d", "--dir", help="Working directory for Claude Code")
545
+ parser.add_argument("-m", "--max-iterations", type=int, default=MAX_ITERATIONS)
546
+ parser.add_argument("-t", "--timeout", type=int, default=DEFAULT_TIMEOUT,
547
+ help="Timeout per iteration in seconds (default: 600)")
548
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
549
+
550
+ args = parser.parse_args()
551
+
552
+ # Clear screen and show banner
553
+ os.system('cls' if os.name == 'nt' else 'clear')
554
+ print(BANNER)
555
+
556
+ # Get bug description
557
+ if args.file:
558
+ bug_input = Path(args.file).read_text(encoding="utf-8")
559
+ elif args.bug:
560
+ bug_input = args.bug
561
+ else:
562
+ print(f" {C.CYAN}Describe the bug (Ctrl+D or Ctrl+Z to finish):{C.RESET}")
563
+ bug_input = sys.stdin.read().strip()
564
+
565
+ if not bug_input:
566
+ print_error_box("No bug description provided")
567
+ return 1
568
+
569
+ # Setup
570
+ session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
571
+ system_prompt = load_prompt()
572
+ working_dir = args.dir or os.getcwd()
573
+
574
+ # Session info
575
+ print_header_box(f"SESSION: {session_id}", C.BRIGHT_BLUE)
576
+ print_status("Working Directory", working_dir, C.WHITE)
577
+ print_status("Max Iterations", str(args.max_iterations), C.WHITE)
578
+ print_status("Timeout", f"{args.timeout // 60} min ({args.timeout}s) per iteration", C.WHITE)
579
+ print_status("Context Mode", "PERSISTENT (--continue)", C.WHITE)
580
+ print_status("System Prompt", f"{len(system_prompt)} chars loaded", C.WHITE)
581
+ print()
582
+
583
+ print_section("BUG REPORT", C.BRIGHT_RED)
584
+ # Wrap bug input nicely
585
+ for line in bug_input.split('\n')[:10]:
586
+ print(f" {C.RED}{line[:80]}{C.RESET}")
587
+ if len(bug_input.split('\n')) > 10:
588
+ print(f" {C.RED}... ({len(bug_input.split(chr(10))) - 10} more lines){C.RESET}")
589
+ print()
590
+
591
+ sys.stdout.flush()
592
+
593
+ # Initial context
594
+ context = f"""
595
+ ## Bug Report
596
+
597
+ {bug_input}
598
+
599
+ ## Instructions
600
+
601
+ Analyze this bug. Gather whatever logs/information you need to understand it.
602
+ Then fix it. Use the reasoning protocol before each action.
603
+ """
604
+
605
+ iteration_history = []
606
+
607
+ for iteration in range(1, args.max_iterations + 1):
608
+ print_iteration_header(iteration, args.max_iterations)
609
+
610
+ if iteration == 1:
611
+ full_prompt = f"{system_prompt}\n\n---\n\n{context}"
612
+ continue_session = False
613
+ else:
614
+ full_prompt = context
615
+ continue_session = True
616
+
617
+ if args.verbose:
618
+ print(f" {C.DIM}[DEBUG] Prompt: {len(full_prompt)} chars, continue={continue_session}{C.RESET}")
619
+
620
+ # Show "Claude is working" indicator
621
+ print(f"\n {C.BRIGHT_BLUE}▶ Claude is working...{C.RESET}\n")
622
+ sys.stdout.flush()
623
+
624
+ # Run Claude with real-time streaming output
625
+ output = run_claude_code(full_prompt, working_dir, continue_session=continue_session, stream=True, timeout=args.timeout)
626
+
627
+ print(f"\n {C.BLUE}✓ Iteration complete{C.RESET}\n")
628
+
629
+ # Save log
630
+ save_log(iteration, output, session_id)
631
+ iteration_history.append(output)
632
+
633
+ # Check exit condition
634
+ signal, should_exit = check_exit_condition(output)
635
+
636
+ if should_exit:
637
+ if signal == "RESOLVED":
638
+ print_success_box(f"Bug fixed in {iteration} iteration(s)!")
639
+ print_status("Session", session_id, C.WHITE)
640
+ print_status("Logs", str(LOG_DIR), C.WHITE)
641
+ print()
642
+ return 0
643
+ elif signal == "CLARIFY":
644
+ print_warning_box("Claude needs clarification")
645
+ print(f"\n {C.BLUE}Your response:{C.RESET}")
646
+ human_input = input(f" {C.WHITE}> {C.RESET}")
647
+ context = f"""
648
+ ## Human Clarification
649
+
650
+ {human_input}
651
+
652
+ Continue debugging with this information. Use the reasoning protocol before each action.
653
+ """
654
+ continue
655
+ elif signal == "BLOCKED":
656
+ print_error_box(f"Claude is blocked - needs human help")
657
+ print_status("Session", session_id, C.WHITE)
658
+ print_status("Logs", str(LOG_DIR), C.WHITE)
659
+ print()
660
+ return 1
661
+ else:
662
+ print_error_box(f"Error occurred: {signal}")
663
+ return 1
664
+
665
+ # Prepare context for next iteration
666
+ context = f"""
667
+ ## Iteration {iteration + 1}
668
+
669
+ The bug is NOT yet resolved. You have full context from previous iterations.
670
+
671
+ Continue debugging. Analyze what happened, determine next steps, and proceed.
672
+ Use the reasoning protocol before each action.
673
+ """
674
+
675
+ # Max iterations reached
676
+ print_warning_box(f"Max iterations ({args.max_iterations}) reached")
677
+ print_status("Session", session_id, C.WHITE)
678
+ print_status("Logs", str(LOG_DIR), C.WHITE)
679
+ print()
680
+
681
+ # Save summary
682
+ summary = f"# DaveLoop Session {session_id}\n\n"
683
+ summary += f"Bug: {bug_input[:200]}...\n\n"
684
+ summary += f"Iterations: {args.max_iterations}\n\n"
685
+ summary += "## Iteration History\n\n"
686
+ for i, hist in enumerate(iteration_history, 1):
687
+ summary += f"### Iteration {i}\n```\n{hist[:500]}...\n```\n\n"
688
+ (LOG_DIR / f"{session_id}_summary.md").write_text(summary, encoding="utf-8")
689
+
690
+ return 1
691
+
692
+
693
+ if __name__ == "__main__":
694
+ sys.exit(main())