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