@jojonax/codex-copilot 1.5.3 → 1.6.0
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.
- package/LICENSE +21 -21
- package/README.md +144 -44
- package/bin/cli.js +189 -182
- package/package.json +39 -39
- package/src/commands/evolve.js +316 -316
- package/src/commands/fix.js +447 -399
- package/src/commands/init.js +298 -298
- package/src/commands/reset.js +61 -61
- package/src/commands/retry.js +190 -153
- package/src/commands/run.js +958 -905
- package/src/commands/skip.js +62 -62
- package/src/commands/status.js +95 -95
- package/src/commands/usage.js +361 -361
- package/src/utils/automator.js +279 -279
- package/src/utils/checkpoint.js +246 -160
- package/src/utils/detect-prd.js +137 -137
- package/src/utils/git.js +388 -133
- package/src/utils/github.js +486 -429
- package/src/utils/json.js +220 -197
- package/src/utils/logger.js +41 -41
- package/src/utils/prompt.js +49 -49
- package/src/utils/provider.js +770 -769
- package/src/utils/self-heal.js +330 -0
- package/src/utils/shell-bootstrap.js +404 -0
- package/src/utils/update-check.js +103 -103
package/src/commands/run.js
CHANGED
|
@@ -1,905 +1,958 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* codex-copilot run - Main orchestration loop
|
|
3
|
-
*
|
|
4
|
-
* Executes tasks one by one: Develop → PR → Review → Fix → Merge → Next
|
|
5
|
-
* Features fine-grained checkpoint/resume at every sub-step.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
-
import { resolve } from 'path';
|
|
10
|
-
import { log, progressBar } from '../utils/logger.js';
|
|
11
|
-
import { git } from '../utils/git.js';
|
|
12
|
-
import { github } from '../utils/github.js';
|
|
13
|
-
import { closePrompt } from '../utils/prompt.js';
|
|
14
|
-
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
15
|
-
import { provider } from '../utils/provider.js';
|
|
16
|
-
import { readJSON, writeJSON } from '../utils/json.js';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
let
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
log.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
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
|
-
log.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
log.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
log.
|
|
119
|
-
log.
|
|
120
|
-
log.info(`
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
log.blank();
|
|
128
|
-
log.
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
task.
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
log.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
log.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
checkpoint.saveStep(task.id, '
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
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
|
-
const
|
|
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
|
-
return;
|
|
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
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
log.
|
|
648
|
-
github.
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
${
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1
|
+
/**
|
|
2
|
+
* codex-copilot run - Main orchestration loop
|
|
3
|
+
*
|
|
4
|
+
* Executes tasks one by one: Develop → PR → Review → Fix → Merge → Next
|
|
5
|
+
* Features fine-grained checkpoint/resume at every sub-step.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { log, progressBar } from '../utils/logger.js';
|
|
11
|
+
import { git } from '../utils/git.js';
|
|
12
|
+
import { github } from '../utils/github.js';
|
|
13
|
+
import { closePrompt } from '../utils/prompt.js';
|
|
14
|
+
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
15
|
+
import { provider } from '../utils/provider.js';
|
|
16
|
+
import { readJSON, writeJSON } from '../utils/json.js';
|
|
17
|
+
import { preFlightCheck, releaseLock } from '../utils/self-heal.js';
|
|
18
|
+
|
|
19
|
+
const maxRateLimitRetries = 3;
|
|
20
|
+
|
|
21
|
+
export async function run(projectDir) {
|
|
22
|
+
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
23
|
+
const configPath = resolve(projectDir, '.codex-copilot/config.json');
|
|
24
|
+
const checkpoint = createCheckpoint(projectDir);
|
|
25
|
+
|
|
26
|
+
let tasks;
|
|
27
|
+
let state;
|
|
28
|
+
|
|
29
|
+
// Load tasks.json (required)
|
|
30
|
+
try {
|
|
31
|
+
tasks = readJSON(tasksPath);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
log.warn(`tasks.json read issue: ${err.message}`);
|
|
34
|
+
log.info('Attempting auto-repair...');
|
|
35
|
+
|
|
36
|
+
// Try auto-fix (deep repair + schema validation)
|
|
37
|
+
const { autoFix } = await import('./fix.js');
|
|
38
|
+
const fixResult = autoFix(projectDir, { files: ['tasks.json'] });
|
|
39
|
+
|
|
40
|
+
if (fixResult.ok && fixResult.repaired.includes('tasks.json')) {
|
|
41
|
+
try {
|
|
42
|
+
tasks = readJSON(tasksPath);
|
|
43
|
+
log.info('✅ tasks.json auto-repaired successfully');
|
|
44
|
+
} catch {
|
|
45
|
+
// Auto-fix wrote the file but it's still not readable (shouldn't happen)
|
|
46
|
+
log.error('Auto-repair wrote file but it remains unreadable');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If still no tasks, try backup
|
|
51
|
+
if (!tasks && existsSync(tasksPath + '.bak')) {
|
|
52
|
+
log.info('Trying backup file...');
|
|
53
|
+
try {
|
|
54
|
+
tasks = readJSON(tasksPath + '.bak');
|
|
55
|
+
writeJSON(tasksPath, tasks);
|
|
56
|
+
log.info('✅ Restored tasks.json from backup');
|
|
57
|
+
} catch {
|
|
58
|
+
log.error('Backup is also corrupted');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!tasks) {
|
|
63
|
+
log.error('Cannot recover tasks.json. Run: codex-copilot fix');
|
|
64
|
+
closePrompt();
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Load state.json (optional — falls back to default)
|
|
70
|
+
try {
|
|
71
|
+
state = checkpoint.load();
|
|
72
|
+
} catch {
|
|
73
|
+
log.warn('State file corrupted — resetting to initial state');
|
|
74
|
+
state = checkpoint.reset();
|
|
75
|
+
}
|
|
76
|
+
const config = existsSync(configPath) ? readJSON(configPath) : {};
|
|
77
|
+
|
|
78
|
+
// Validate tasks.json structure
|
|
79
|
+
if (!tasks.tasks || !Array.isArray(tasks.tasks) || !tasks.total) {
|
|
80
|
+
log.error('Invalid tasks.json format: missing tasks array or total field');
|
|
81
|
+
log.warn('Please re-run codex-copilot init and let CodeX regenerate tasks.json');
|
|
82
|
+
closePrompt();
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const baseBranch = config.base_branch || 'main';
|
|
87
|
+
const providerId = config.provider || 'codex-cli';
|
|
88
|
+
const maxReviewRounds = config.max_review_rounds || 2;
|
|
89
|
+
const pollInterval = config.review_poll_interval || 60;
|
|
90
|
+
const waitTimeout = config.review_wait_timeout || 600;
|
|
91
|
+
const isPrivate = github.isPrivateRepo(projectDir); // Cache once
|
|
92
|
+
const weeklyQuotaThreshold = config.weekly_quota_threshold || 97;
|
|
93
|
+
|
|
94
|
+
const providerInfo = provider.getProvider(providerId);
|
|
95
|
+
log.info(`AI Provider: ${providerInfo ? providerInfo.name : providerId}`);
|
|
96
|
+
|
|
97
|
+
// ===== Graceful shutdown handler =====
|
|
98
|
+
let shuttingDown = false;
|
|
99
|
+
const gracefulShutdown = () => {
|
|
100
|
+
if (shuttingDown) {
|
|
101
|
+
log.warn('Force exit');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
shuttingDown = true;
|
|
105
|
+
log.blank();
|
|
106
|
+
log.warn('Interrupt received — saving checkpoint...');
|
|
107
|
+
// State is already saved at each step, just need to save tasks.json
|
|
108
|
+
writeJSON(tasksPath, tasks);
|
|
109
|
+
releaseLock(projectDir);
|
|
110
|
+
log.info('✅ Checkpoint saved. Run `codex-copilot run` to resume.');
|
|
111
|
+
log.blank();
|
|
112
|
+
closePrompt();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
};
|
|
115
|
+
process.on('SIGINT', gracefulShutdown);
|
|
116
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
117
|
+
|
|
118
|
+
log.title('🚀 Starting automated development loop');
|
|
119
|
+
log.blank();
|
|
120
|
+
log.info(`Project: ${tasks.project}`);
|
|
121
|
+
log.info(`Total tasks: ${tasks.total}`);
|
|
122
|
+
log.info(`Base branch: ${baseBranch}`);
|
|
123
|
+
|
|
124
|
+
// ===== Pre-flight self-healing =====
|
|
125
|
+
const health = preFlightCheck(projectDir, baseBranch, { checkpoint, tasks });
|
|
126
|
+
if (!health.ok) {
|
|
127
|
+
log.blank();
|
|
128
|
+
log.error('Pre-flight check failed — cannot proceed:');
|
|
129
|
+
for (const b of health.blockers) {
|
|
130
|
+
log.error(` 🛑 ${b}`);
|
|
131
|
+
}
|
|
132
|
+
log.blank();
|
|
133
|
+
releaseLock(projectDir);
|
|
134
|
+
closePrompt();
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
log.blank();
|
|
138
|
+
|
|
139
|
+
// ===== Pre-flight: ensure base branch is committed & pushed =====
|
|
140
|
+
await ensureBaseReady(projectDir, baseBranch, isPrivate);
|
|
141
|
+
|
|
142
|
+
// Show resume info if resuming mid-task
|
|
143
|
+
if (state.phase && state.current_task > 0) {
|
|
144
|
+
log.blank();
|
|
145
|
+
log.info(`⏩ Resuming task #${state.current_task} from: ${state.phase} → ${state.phase_step}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ===== Auto-retry blocked tasks =====
|
|
149
|
+
// On each run, reset blocked tasks to pending so they're retried in order.
|
|
150
|
+
// Branches and checkpoint steps are PRESERVED — the task resumes from where
|
|
151
|
+
// it left off, keeping all previously developed code.
|
|
152
|
+
const blockedTasks = tasks.tasks.filter(t => t.status === 'blocked');
|
|
153
|
+
if (blockedTasks.length > 0) {
|
|
154
|
+
log.blank();
|
|
155
|
+
log.info(`🔄 Auto-retrying ${blockedTasks.length} blocked task(s)...`);
|
|
156
|
+
for (const bt of blockedTasks) {
|
|
157
|
+
bt.status = 'pending';
|
|
158
|
+
bt.retry_count = (bt.retry_count || 0) + 1;
|
|
159
|
+
bt._retrying = true; // Flag: don't skip even if below checkpoint
|
|
160
|
+
log.dim(` ↳ Task #${bt.id}: ${bt.title.substring(0, 50)} (attempt ${bt.retry_count + 1})`);
|
|
161
|
+
}
|
|
162
|
+
writeJSON(tasksPath, tasks);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const completedCount = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
166
|
+
log.blank();
|
|
167
|
+
progressBar(completedCount, tasks.total, `${completedCount}/${tasks.total} tasks done`);
|
|
168
|
+
log.blank();
|
|
169
|
+
|
|
170
|
+
// Execute tasks one by one
|
|
171
|
+
for (const task of tasks.tasks) {
|
|
172
|
+
// Skip fully completed tasks
|
|
173
|
+
if (task.status === 'completed' || task.status === 'skipped') continue;
|
|
174
|
+
|
|
175
|
+
// Skip tasks whose ID is below the checkpoint (unless retrying a blocked task)
|
|
176
|
+
const isResumingTask = state.current_task === task.id && state.phase;
|
|
177
|
+
if (task.id < state.current_task && !isResumingTask && !task._retrying) continue;
|
|
178
|
+
|
|
179
|
+
// Clean up retry flag (used only to bypass checkpoint skip)
|
|
180
|
+
delete task._retrying;
|
|
181
|
+
|
|
182
|
+
log.blank();
|
|
183
|
+
log.title(`━━━ Task ${task.id}/${tasks.total}: ${task.title} ━━━`);
|
|
184
|
+
log.blank();
|
|
185
|
+
|
|
186
|
+
// ===== Quota pre-check (Codex CLI only) =====
|
|
187
|
+
if (providerId === 'codex-cli' || providerId === 'codex-desktop') {
|
|
188
|
+
const quota = provider.checkQuotaBeforeExecution(weeklyQuotaThreshold);
|
|
189
|
+
if (!quota.ok) {
|
|
190
|
+
log.blank();
|
|
191
|
+
log.error(`⚠ Weekly quota at ${quota.quota7d}% (threshold: ${weeklyQuotaThreshold}%) — stopping to preserve remaining quota`);
|
|
192
|
+
log.info('Run `codex-copilot usage` to check quota details');
|
|
193
|
+
log.info('Run `codex-copilot run` again when quota resets');
|
|
194
|
+
writeJSON(tasksPath, tasks);
|
|
195
|
+
closePrompt();
|
|
196
|
+
process.exit(0);
|
|
197
|
+
}
|
|
198
|
+
if (quota.warning) {
|
|
199
|
+
log.warn(`⚠ Weekly quota at ${quota.quota7d}% — approaching limit`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check dependencies — completed, skipped, and blocked all satisfy dependencies.
|
|
204
|
+
// Blocked tasks are treated as "done for now" to prevent cascade-skipping;
|
|
205
|
+
// the user can retry them later with `codex-copilot retry`.
|
|
206
|
+
if (task.depends_on && task.depends_on.length > 0) {
|
|
207
|
+
const DONE_STATUSES = ['completed', 'skipped', 'blocked'];
|
|
208
|
+
const unfinished = task.depends_on.filter(dep => {
|
|
209
|
+
const depTask = tasks.tasks.find(t => t.id === dep);
|
|
210
|
+
return depTask && !DONE_STATUSES.includes(depTask.status);
|
|
211
|
+
});
|
|
212
|
+
if (unfinished.length > 0) {
|
|
213
|
+
log.warn(`Unfinished dependencies: ${unfinished.join(', ')} — skipping`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// Warn about blocked deps but still proceed
|
|
217
|
+
const blockedDeps = task.depends_on.filter(dep => {
|
|
218
|
+
const depTask = tasks.tasks.find(t => t.id === dep);
|
|
219
|
+
return depTask && depTask.status === 'blocked';
|
|
220
|
+
});
|
|
221
|
+
if (blockedDeps.length > 0) {
|
|
222
|
+
log.warn(`⚠ Dependencies ${blockedDeps.join(', ')} are blocked — proceeding anyway`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ===== Ensure task has all required fields =====
|
|
227
|
+
// Auto-generate branch name if missing (common after corruption recovery)
|
|
228
|
+
if (!task.branch) {
|
|
229
|
+
// Try to recover from checkpoint state
|
|
230
|
+
if (state.current_task === task.id && state.branch) {
|
|
231
|
+
task.branch = state.branch;
|
|
232
|
+
log.dim(` ↳ Recovered branch name from checkpoint: ${task.branch}`);
|
|
233
|
+
} else {
|
|
234
|
+
const slug = (task.title || `task-${task.id}`)
|
|
235
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').substring(0, 40);
|
|
236
|
+
task.branch = `feature/${String(task.id).padStart(3, '0')}-${slug}`;
|
|
237
|
+
log.dim(` ↳ Generated branch name: ${task.branch}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!task.depends_on) task.depends_on = [];
|
|
241
|
+
if (!task.acceptance) task.acceptance = [];
|
|
242
|
+
if (!task.description) task.description = task.title;
|
|
243
|
+
|
|
244
|
+
// Mark task as in_progress
|
|
245
|
+
task.status = 'in_progress';
|
|
246
|
+
const isRetry = (task.retry_count || 0) > 0;
|
|
247
|
+
const retryStartedAt = isRetry ? new Date().toISOString() : null;
|
|
248
|
+
writeJSON(tasksPath, tasks);
|
|
249
|
+
|
|
250
|
+
// ===== Phase 1: Develop =====
|
|
251
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'codex_complete')) {
|
|
252
|
+
await developPhase(projectDir, task, baseBranch, checkpoint, providerId);
|
|
253
|
+
// If dev phase failed, mark blocked and skip to next task
|
|
254
|
+
if (task.status === 'blocked') {
|
|
255
|
+
writeJSON(tasksPath, tasks);
|
|
256
|
+
log.blank();
|
|
257
|
+
log.warn(`⚠ Task #${task.id} is blocked — AI development failed`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
log.dim('⏩ Skipping develop phase (already completed)');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ===== Phase 2: Create PR =====
|
|
265
|
+
let prInfo;
|
|
266
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'pr_created')) {
|
|
267
|
+
prInfo = await prPhase(projectDir, task, baseBranch, checkpoint, isPrivate);
|
|
268
|
+
} else {
|
|
269
|
+
// PR already created, load from state
|
|
270
|
+
state = checkpoint.load();
|
|
271
|
+
prInfo = { number: state.current_pr, url: '' };
|
|
272
|
+
log.dim(`⏩ Skipping PR phase (PR #${prInfo.number} already created)`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ===== Phase 3: Review loop =====
|
|
276
|
+
if (!checkpoint.isStepDone(task.id, 'merge', 'merged')) {
|
|
277
|
+
// On retry: request fresh re-review since old reviews may be stale
|
|
278
|
+
if (isRetry && prInfo?.number) {
|
|
279
|
+
log.info('🔄 Retry: requesting fresh re-review on existing PR...');
|
|
280
|
+
try {
|
|
281
|
+
github.requestReReview(projectDir, prInfo.number);
|
|
282
|
+
} catch { /* ignore — review bots may not be configured */ }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await reviewLoop(projectDir, task, prInfo, {
|
|
286
|
+
maxRounds: maxReviewRounds,
|
|
287
|
+
pollInterval,
|
|
288
|
+
waitTimeout,
|
|
289
|
+
retryStartedAt, // Only count reviews after this timestamp
|
|
290
|
+
}, checkpoint, providerId, isPrivate);
|
|
291
|
+
} else {
|
|
292
|
+
log.dim('⏩ Skipping review phase (already completed)');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ===== Phase 4: Merge =====
|
|
296
|
+
if (!checkpoint.isStepDone(task.id, 'merge', 'merged')) {
|
|
297
|
+
await mergePhase(projectDir, task, prInfo, baseBranch, checkpoint);
|
|
298
|
+
} else {
|
|
299
|
+
// Checkpoint says merged — verify against GitHub
|
|
300
|
+
if (prInfo?.number) {
|
|
301
|
+
const prState = github.getPRState(projectDir, prInfo.number);
|
|
302
|
+
if (prState !== 'merged') {
|
|
303
|
+
log.warn(`⚠ Checkpoint says merged but PR #${prInfo.number} is ${prState} — re-entering merge`);
|
|
304
|
+
await mergePhase(projectDir, task, prInfo, baseBranch, checkpoint);
|
|
305
|
+
} else {
|
|
306
|
+
log.dim('⏩ Skipping merge phase (PR confirmed merged on GitHub)');
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
log.dim('⏩ Skipping merge phase (already merged)');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if task was blocked during review/merge
|
|
314
|
+
if (task.status === 'blocked') {
|
|
315
|
+
writeJSON(tasksPath, tasks);
|
|
316
|
+
log.blank();
|
|
317
|
+
log.warn(`⚠ Task #${task.id} is blocked — needs manual intervention`);
|
|
318
|
+
log.blank();
|
|
319
|
+
const done = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
320
|
+
const blocked = tasks.tasks.filter(t => t.status === 'blocked').length;
|
|
321
|
+
progressBar(done, tasks.total, `${done}/${tasks.total} done, ${blocked} blocked`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Mark task complete
|
|
326
|
+
task.status = 'completed';
|
|
327
|
+
delete task.block_reason;
|
|
328
|
+
delete task.retry_count;
|
|
329
|
+
writeJSON(tasksPath, tasks);
|
|
330
|
+
checkpoint.completeTask(task.id);
|
|
331
|
+
|
|
332
|
+
log.blank();
|
|
333
|
+
const done = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
334
|
+
progressBar(done, tasks.total, `${done}/${tasks.total} tasks done`);
|
|
335
|
+
log.info(`✅ Task #${task.id} complete!`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
log.blank();
|
|
339
|
+
const finalDone = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
340
|
+
const finalBlocked = tasks.tasks.filter(t => t.status === 'blocked').length;
|
|
341
|
+
if (finalBlocked > 0) {
|
|
342
|
+
log.title(`✅ Finished — ${finalDone}/${tasks.total} done, ${finalBlocked} blocked`);
|
|
343
|
+
} else {
|
|
344
|
+
log.title('🎉 All tasks complete!');
|
|
345
|
+
}
|
|
346
|
+
log.blank();
|
|
347
|
+
process.removeListener('SIGINT', gracefulShutdown);
|
|
348
|
+
process.removeListener('SIGTERM', gracefulShutdown);
|
|
349
|
+
releaseLock(projectDir);
|
|
350
|
+
|
|
351
|
+
// ===== Auto-evolve: trigger next round if enabled =====
|
|
352
|
+
if (config.auto_evolve !== false) {
|
|
353
|
+
log.blank();
|
|
354
|
+
log.info('🔄 Auto-evolving to next round...');
|
|
355
|
+
log.blank();
|
|
356
|
+
const { evolve } = await import('./evolve.js');
|
|
357
|
+
await evolve(projectDir);
|
|
358
|
+
return; // evolve() calls run() internally
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
closePrompt();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ──────────────────────────────────────────────
|
|
365
|
+
// Phase 1: Develop
|
|
366
|
+
// ──────────────────────────────────────────────
|
|
367
|
+
async function developPhase(projectDir, task, baseBranch, checkpoint, providerId) {
|
|
368
|
+
log.step('Phase 1/4: Develop');
|
|
369
|
+
|
|
370
|
+
// Step 1: Switch to feature branch
|
|
371
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'branch_created')) {
|
|
372
|
+
try {
|
|
373
|
+
git.checkoutBranch(projectDir, task.branch, baseBranch);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
log.error(`Failed to switch to branch ${task.branch}: ${err.message}`);
|
|
376
|
+
task.status = 'blocked';
|
|
377
|
+
task.block_reason = 'git_checkout_failed';
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
log.info(`Switched to branch: ${task.branch}`);
|
|
381
|
+
checkpoint.saveStep(task.id, 'develop', 'branch_created', { branch: task.branch });
|
|
382
|
+
} else {
|
|
383
|
+
// Ensure we're on the right branch
|
|
384
|
+
const current = git.currentBranch(projectDir);
|
|
385
|
+
if (current !== task.branch) {
|
|
386
|
+
try {
|
|
387
|
+
git.checkoutBranch(projectDir, task.branch, baseBranch);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
log.error(`Failed to switch to branch ${task.branch}: ${err.message}`);
|
|
390
|
+
task.status = 'blocked';
|
|
391
|
+
task.block_reason = 'git_checkout_failed';
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
log.dim('⏩ Branch already created');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Step 2: Build development prompt
|
|
399
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'prompt_ready')) {
|
|
400
|
+
const devPrompt = buildDevPrompt(task, projectDir);
|
|
401
|
+
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
402
|
+
writeFileSync(promptPath, devPrompt);
|
|
403
|
+
checkpoint.saveStep(task.id, 'develop', 'prompt_ready', { branch: task.branch });
|
|
404
|
+
log.info('Development prompt generated');
|
|
405
|
+
} else {
|
|
406
|
+
log.dim('⏩ Prompt already generated');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Step 3: Execute via AI Provider (with rate limit auto-retry + timeout protection)
|
|
410
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'codex_complete')) {
|
|
411
|
+
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
412
|
+
let rateLimitRetries = 0;
|
|
413
|
+
const AI_TIMEOUT_MS = 30 * 60 * 1000; // D4: 30 minute timeout for AI execution
|
|
414
|
+
|
|
415
|
+
while (rateLimitRetries < maxRateLimitRetries) {
|
|
416
|
+
// D4: Race the AI execution against a timeout to prevent infinite hangs
|
|
417
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
418
|
+
setTimeout(() => reject(new Error('AI_TIMEOUT')), AI_TIMEOUT_MS)
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
let result;
|
|
422
|
+
try {
|
|
423
|
+
result = await Promise.race([
|
|
424
|
+
provider.executePrompt(providerId, promptPath, projectDir),
|
|
425
|
+
timeoutPromise,
|
|
426
|
+
]);
|
|
427
|
+
} catch (timeoutErr) {
|
|
428
|
+
if (timeoutErr.message === 'AI_TIMEOUT') {
|
|
429
|
+
log.error(`AI provider timed out after ${AI_TIMEOUT_MS / 60000} minutes — marking task as blocked`);
|
|
430
|
+
task.status = 'blocked';
|
|
431
|
+
task.block_reason = 'ai_timeout';
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
throw timeoutErr;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (result.ok) {
|
|
438
|
+
log.info('Development complete');
|
|
439
|
+
checkpoint.saveStep(task.id, 'develop', 'codex_complete', { branch: task.branch });
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (result.rateLimited && result.retryAt) {
|
|
444
|
+
rateLimitRetries++;
|
|
445
|
+
log.warn(`Rate limit hit (attempt ${rateLimitRetries}/${maxRateLimitRetries})`);
|
|
446
|
+
if (rateLimitRetries >= maxRateLimitRetries) {
|
|
447
|
+
log.error('Max rate limit retries reached — marking task as blocked');
|
|
448
|
+
task.status = 'blocked';
|
|
449
|
+
task.block_reason = 'rate_limited';
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
await waitForRateLimitReset(result.retryAt, result.retryAtStr);
|
|
453
|
+
log.info('Rate limit reset — resuming development...');
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Non-rate-limit failure
|
|
458
|
+
log.error('AI development failed — marking task as blocked');
|
|
459
|
+
task.status = 'blocked';
|
|
460
|
+
task.block_reason = 'dev_failed';
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ──────────────────────────────────────────────
|
|
467
|
+
// Phase 2: Create PR
|
|
468
|
+
// ──────────────────────────────────────────────
|
|
469
|
+
async function prPhase(projectDir, task, baseBranch, checkpoint, isPrivate) {
|
|
470
|
+
log.step('Phase 2/4: Submit PR');
|
|
471
|
+
|
|
472
|
+
// Step 1: Commit changes
|
|
473
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'committed')) {
|
|
474
|
+
if (!git.isClean(projectDir)) {
|
|
475
|
+
log.info('Uncommitted changes detected, auto-committing...');
|
|
476
|
+
const skipCI = isPrivate ? ' [skip ci]' : '';
|
|
477
|
+
git.commitAll(projectDir, `feat(task-${task.id}): ${task.title}${skipCI}`);
|
|
478
|
+
}
|
|
479
|
+
checkpoint.saveStep(task.id, 'pr', 'committed', { branch: task.branch });
|
|
480
|
+
log.info('Changes committed');
|
|
481
|
+
} else {
|
|
482
|
+
log.dim('⏩ Changes already committed');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Step 2: Push
|
|
486
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'pushed')) {
|
|
487
|
+
git.pushBranch(projectDir, task.branch);
|
|
488
|
+
checkpoint.saveStep(task.id, 'pr', 'pushed', { branch: task.branch });
|
|
489
|
+
log.info('Code pushed');
|
|
490
|
+
} else {
|
|
491
|
+
log.dim('⏩ Code already pushed');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Step 3: Create PR
|
|
495
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'pr_created')) {
|
|
496
|
+
const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
|
|
497
|
+
? task.acceptance.map(a => `- [ ] ${a}`).join('\n')
|
|
498
|
+
: '- [ ] Feature works correctly';
|
|
499
|
+
const prBody = `## Task #${task.id}: ${task.title}\n\n${task.description}\n\n### Acceptance Criteria\n${acceptanceList}\n\n---\n*Auto-created by Codex-Copilot*`;
|
|
500
|
+
|
|
501
|
+
// Use auto-recovery: create → find existing → fix remote → retry
|
|
502
|
+
const prInfo = github.createPRWithRecovery(projectDir, {
|
|
503
|
+
title: `feat(task-${task.id}): ${task.title}`,
|
|
504
|
+
body: prBody,
|
|
505
|
+
base: baseBranch,
|
|
506
|
+
head: task.branch,
|
|
507
|
+
});
|
|
508
|
+
log.info(`PR ready: #${prInfo.number} ${prInfo.url}`);
|
|
509
|
+
|
|
510
|
+
checkpoint.saveStep(task.id, 'pr', 'pr_created', {
|
|
511
|
+
branch: task.branch,
|
|
512
|
+
current_pr: prInfo.number,
|
|
513
|
+
});
|
|
514
|
+
return prInfo;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// If we get here, PR was already created — load from state
|
|
518
|
+
const state = checkpoint.load();
|
|
519
|
+
return { number: state.current_pr, url: '' };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ──────────────────────────────────────────────
|
|
523
|
+
// Phase 3: Review loop
|
|
524
|
+
// ──────────────────────────────────────────────
|
|
525
|
+
async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout, retryStartedAt }, checkpoint, providerId, isPrivate) {
|
|
526
|
+
const HARD_MAX_ROUNDS = 5;
|
|
527
|
+
const MAX_POLL_RETRIES = 3;
|
|
528
|
+
let maxRounds = Math.min(_maxRounds, HARD_MAX_ROUNDS);
|
|
529
|
+
log.step('Phase 3/4: Waiting for review');
|
|
530
|
+
|
|
531
|
+
// Resume from saved review round
|
|
532
|
+
const state = checkpoint.load();
|
|
533
|
+
const startRound = (state.current_task === task.id && state.review_round > 0)
|
|
534
|
+
? state.review_round
|
|
535
|
+
: 1;
|
|
536
|
+
|
|
537
|
+
let pollRetries = 0;
|
|
538
|
+
|
|
539
|
+
for (let round = startRound; round <= maxRounds; round++) {
|
|
540
|
+
// Step: Waiting for review
|
|
541
|
+
checkpoint.saveStep(task.id, 'review', 'waiting_review', {
|
|
542
|
+
branch: task.branch,
|
|
543
|
+
current_pr: prInfo.number,
|
|
544
|
+
review_round: round,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
log.info('Checking for review feedback...');
|
|
548
|
+
|
|
549
|
+
let gotReview = false;
|
|
550
|
+
|
|
551
|
+
// Always proactively check for existing reviews first.
|
|
552
|
+
// This catches: already-posted bot reviews, stale reviews found on resume,
|
|
553
|
+
// and fast bot responses after fix pushes.
|
|
554
|
+
const existingReviews = github.getReviews(projectDir, prInfo.number);
|
|
555
|
+
const existingComments = github.getIssueComments(projectDir, prInfo.number);
|
|
556
|
+
|
|
557
|
+
// On retry: only count reviews posted AFTER the retry started
|
|
558
|
+
const isReviewFresh = (item) => {
|
|
559
|
+
if (!retryStartedAt) return true; // Not a retry — all reviews are valid
|
|
560
|
+
const itemDate = item.submitted_at || item.created_at || item.updated_at;
|
|
561
|
+
return itemDate && new Date(itemDate) > new Date(retryStartedAt);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const freshReviews = existingReviews.filter(isReviewFresh);
|
|
565
|
+
const freshComments = existingComments.filter(isReviewFresh);
|
|
566
|
+
|
|
567
|
+
const hasReview = freshReviews.some(r => r.state !== 'PENDING');
|
|
568
|
+
const hasBotComment = freshComments.some(c =>
|
|
569
|
+
c.user?.type === 'Bot' || c.user?.login?.includes('bot')
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (hasReview || hasBotComment) {
|
|
573
|
+
log.info('Review found — processing immediately');
|
|
574
|
+
gotReview = true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!gotReview) {
|
|
578
|
+
// After fix pushes (round > 1), bot should respond quickly if configured.
|
|
579
|
+
// Use shorter timeout to avoid wasting 10+ minutes waiting for nothing.
|
|
580
|
+
const effectiveTimeout = round > 1 ? Math.min(waitTimeout, 120) : waitTimeout;
|
|
581
|
+
log.info(`Waiting for ${round > 1 ? 'new ' : ''}review... (timeout: ${effectiveTimeout}s)`);
|
|
582
|
+
gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, effectiveTimeout);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!gotReview) {
|
|
586
|
+
// Before giving up, do one last proactive check
|
|
587
|
+
const lastChance = github.getLatestReviewState(projectDir, prInfo.number);
|
|
588
|
+
if (lastChance === 'APPROVED') {
|
|
589
|
+
log.info('✅ Review approved (found on final check)!');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const lastFeedback = github.collectReviewFeedback(projectDir, prInfo.number);
|
|
593
|
+
if (lastFeedback) {
|
|
594
|
+
log.info('Found existing feedback — processing');
|
|
595
|
+
gotReview = true;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!gotReview) {
|
|
600
|
+
pollRetries++;
|
|
601
|
+
if (pollRetries >= MAX_POLL_RETRIES) {
|
|
602
|
+
log.error(`Review polling timed out ${MAX_POLL_RETRIES} times — marking task as blocked`);
|
|
603
|
+
task.status = 'blocked';
|
|
604
|
+
task.block_reason = 'review_timeout';
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
log.warn(`Review wait timed out — auto-retrying (${pollRetries}/${MAX_POLL_RETRIES})...`);
|
|
608
|
+
round--; // retry same round
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
pollRetries = 0; // reset on success
|
|
612
|
+
|
|
613
|
+
// Step: Feedback received
|
|
614
|
+
checkpoint.saveStep(task.id, 'review', 'feedback_received', {
|
|
615
|
+
branch: task.branch,
|
|
616
|
+
current_pr: prInfo.number,
|
|
617
|
+
review_round: round,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Check review status
|
|
621
|
+
const reviewState = github.getLatestReviewState(projectDir, prInfo.number);
|
|
622
|
+
log.info(`Review status: ${reviewState}`);
|
|
623
|
+
|
|
624
|
+
if (reviewState === 'APPROVED') {
|
|
625
|
+
log.info('✅ Review approved!');
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Collect review feedback (raw — classification done by AI)
|
|
630
|
+
const feedback = github.collectReviewFeedback(projectDir, prInfo.number);
|
|
631
|
+
if (!feedback) {
|
|
632
|
+
log.info('No review feedback found — proceeding ✅');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Use AI to classify the review feedback
|
|
637
|
+
log.info('Classifying review feedback via AI...');
|
|
638
|
+
const classification = await provider.classifyReview(providerId, feedback, projectDir);
|
|
639
|
+
|
|
640
|
+
if (classification === 'pass') {
|
|
641
|
+
log.info('AI determined no actionable issues — proceeding ✅');
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (classification === null) {
|
|
646
|
+
// AI classification failed — fall back to structural GitHub API signals
|
|
647
|
+
log.dim('AI classification unavailable, using structural fallback');
|
|
648
|
+
const inlineComments = github.getReviewComments(projectDir, prInfo.number);
|
|
649
|
+
const hasInlineComments = inlineComments && inlineComments.length > 0;
|
|
650
|
+
|
|
651
|
+
if (reviewState === 'CHANGES_REQUESTED') {
|
|
652
|
+
log.info('Review state: CHANGES_REQUESTED — entering fix phase');
|
|
653
|
+
} else if (reviewState === 'COMMENTED' && !hasInlineComments) {
|
|
654
|
+
log.info('COMMENTED with no inline code comments — treating as passed ✅');
|
|
655
|
+
return;
|
|
656
|
+
} else if (!hasInlineComments) {
|
|
657
|
+
log.info('No inline code comments found — treating as passed ✅');
|
|
658
|
+
return;
|
|
659
|
+
} else {
|
|
660
|
+
log.info(`Found ${inlineComments.length} inline code comment(s) — entering fix phase`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// AI says FIX, or structural fallback indicates issues
|
|
665
|
+
log.blank();
|
|
666
|
+
log.warn(`Received review feedback (round ${round}/${maxRounds})`);
|
|
667
|
+
|
|
668
|
+
if (round >= maxRounds) {
|
|
669
|
+
if (maxRounds < HARD_MAX_ROUNDS) {
|
|
670
|
+
// Auto-extend: give it one more round
|
|
671
|
+
maxRounds++;
|
|
672
|
+
log.warn(`Auto-extending fix rounds to ${maxRounds}`);
|
|
673
|
+
} else {
|
|
674
|
+
// Hard limit reached — cannot keep fixing forever
|
|
675
|
+
log.error(`Hard limit of ${HARD_MAX_ROUNDS} fix rounds reached — marking task as blocked`);
|
|
676
|
+
log.error('This task needs manual intervention to resolve review issues');
|
|
677
|
+
task.status = 'blocked';
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Let AI fix based on the specific review feedback
|
|
683
|
+
await fixPhase(projectDir, task, feedback, round, providerId);
|
|
684
|
+
|
|
685
|
+
// Step: Fix applied
|
|
686
|
+
checkpoint.saveStep(task.id, 'review', 'fix_applied', {
|
|
687
|
+
branch: task.branch,
|
|
688
|
+
current_pr: prInfo.number,
|
|
689
|
+
review_round: round,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Push fix — only if there are actual changes
|
|
693
|
+
if (!git.isClean(projectDir)) {
|
|
694
|
+
const skipCI = isPrivate ? ' [skip ci]' : '';
|
|
695
|
+
git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})${skipCI}`);
|
|
696
|
+
git.pushBranch(projectDir, task.branch);
|
|
697
|
+
log.info('Fix pushed');
|
|
698
|
+
|
|
699
|
+
// Request bot to re-review the updated code
|
|
700
|
+
log.info('Requesting re-review...');
|
|
701
|
+
github.requestReReview(projectDir, prInfo.number);
|
|
702
|
+
|
|
703
|
+
// Wait for review bot to react
|
|
704
|
+
await sleep(15000);
|
|
705
|
+
} else {
|
|
706
|
+
// AI fix produced no code changes — it cannot resolve this issue
|
|
707
|
+
log.error('AI fix produced no changes — marking task as blocked');
|
|
708
|
+
log.error('This task needs manual code changes to resolve review issues');
|
|
709
|
+
task.status = 'blocked';
|
|
710
|
+
task.block_reason = 'review_failed';
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
717
|
+
let elapsed = 0;
|
|
718
|
+
const startReviewCount = github.getReviews(projectDir, prNumber).length;
|
|
719
|
+
const startCommentCount = github.getIssueComments(projectDir, prNumber).length;
|
|
720
|
+
|
|
721
|
+
// Spinner for visual feedback during polling
|
|
722
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
723
|
+
let spinIdx = 0;
|
|
724
|
+
const spinTimer = setInterval(() => {
|
|
725
|
+
const frame = SPINNER[spinIdx % SPINNER.length];
|
|
726
|
+
const remaining = Math.max(0, timeout - elapsed);
|
|
727
|
+
process.stdout.write(`\r\x1b[K \x1b[36m${frame}\x1b[0m Waiting for review... (${remaining}s remaining)`);
|
|
728
|
+
spinIdx++;
|
|
729
|
+
}, 80);
|
|
730
|
+
|
|
731
|
+
while (elapsed < timeout) {
|
|
732
|
+
await sleep(pollInterval * 1000);
|
|
733
|
+
elapsed += pollInterval;
|
|
734
|
+
|
|
735
|
+
const currentReviews = github.getReviews(projectDir, prNumber);
|
|
736
|
+
const currentComments = github.getIssueComments(projectDir, prNumber);
|
|
737
|
+
|
|
738
|
+
const hasNewReview = currentReviews.length > startReviewCount;
|
|
739
|
+
const hasNewBotComment = currentComments.length > startCommentCount &&
|
|
740
|
+
currentComments.some(c => c.user?.type === 'Bot' || c.user?.login?.includes('bot'));
|
|
741
|
+
|
|
742
|
+
if (hasNewReview || hasNewBotComment) {
|
|
743
|
+
clearInterval(spinTimer);
|
|
744
|
+
process.stdout.write('\r\x1b[K');
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
clearInterval(spinTimer);
|
|
749
|
+
process.stdout.write('\r\x1b[K');
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ──────────────────────────────────────────────
|
|
754
|
+
// Fix phase
|
|
755
|
+
// ──────────────────────────────────────────────
|
|
756
|
+
async function fixPhase(projectDir, task, feedback, round, providerId) {
|
|
757
|
+
log.step(`Fixing review comments (round ${round})`);
|
|
758
|
+
|
|
759
|
+
const fixPrompt = `Please fix the following review comments on Task #${task.id}: ${task.title}
|
|
760
|
+
|
|
761
|
+
## Review Feedback
|
|
762
|
+
${feedback}
|
|
763
|
+
|
|
764
|
+
## Requirements
|
|
765
|
+
1. Fix each issue listed above
|
|
766
|
+
2. Suggestions (non-blocking) can be skipped — explain why in the commit message
|
|
767
|
+
3. Ensure fixes don't introduce new issues
|
|
768
|
+
4. Do NOT run git add or git commit — the automation handles committing
|
|
769
|
+
`;
|
|
770
|
+
|
|
771
|
+
// Save to file and execute via provider (with rate limit auto-retry)
|
|
772
|
+
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
773
|
+
writeFileSync(promptPath, fixPrompt);
|
|
774
|
+
|
|
775
|
+
let rateLimitRetries = 0;
|
|
776
|
+
while (rateLimitRetries < maxRateLimitRetries) {
|
|
777
|
+
const result = await provider.executePrompt(providerId, promptPath, projectDir);
|
|
778
|
+
if (result.ok) {
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
if (result.rateLimited && result.retryAt) {
|
|
782
|
+
rateLimitRetries++;
|
|
783
|
+
if (rateLimitRetries >= maxRateLimitRetries) {
|
|
784
|
+
log.warn('Max rate limit retries reached during fix phase');
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
log.warn(`Rate limit hit during fix — waiting for reset...`);
|
|
788
|
+
await waitForRateLimitReset(result.retryAt, result.retryAtStr);
|
|
789
|
+
log.info('Rate limit reset — retrying fix...');
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
break; // Non-rate-limit failure
|
|
793
|
+
}
|
|
794
|
+
log.info('Fix complete');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ──────────────────────────────────────────────
|
|
798
|
+
// Phase 4: Merge
|
|
799
|
+
// ──────────────────────────────────────────────
|
|
800
|
+
async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
|
|
801
|
+
log.step('Phase 4/4: Merge PR');
|
|
802
|
+
|
|
803
|
+
// Skip merge if task was blocked during review
|
|
804
|
+
if (task.status === 'blocked') {
|
|
805
|
+
log.warn(`Task #${task.id} is blocked — skipping merge`);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
log.info(`Auto-merging PR #${prInfo.number}...`);
|
|
810
|
+
|
|
811
|
+
let merged = false;
|
|
812
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
813
|
+
try {
|
|
814
|
+
github.mergePR(projectDir, prInfo.number);
|
|
815
|
+
log.info(`PR #${prInfo.number} merged ✅`);
|
|
816
|
+
merged = true;
|
|
817
|
+
break;
|
|
818
|
+
} catch (err) {
|
|
819
|
+
log.warn(`Merge attempt ${attempt}/3 failed: ${err.message}`);
|
|
820
|
+
if (attempt < 3) {
|
|
821
|
+
log.info('Retrying in 10s...');
|
|
822
|
+
await sleep(10000);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!merged) {
|
|
828
|
+
log.error('Merge failed after 3 attempts — marking task as blocked');
|
|
829
|
+
task.status = 'blocked';
|
|
830
|
+
task.block_reason = 'merge_failed';
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
checkpoint.saveStep(task.id, 'merge', 'merged', {
|
|
835
|
+
branch: task.branch,
|
|
836
|
+
current_pr: prInfo.number,
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Switch back to main branch
|
|
840
|
+
git.checkoutMain(projectDir, baseBranch);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ──────────────────────────────────────────────
|
|
844
|
+
// Pre-flight: ensure base branch has commits & is pushed
|
|
845
|
+
// ──────────────────────────────────────────────
|
|
846
|
+
async function ensureBaseReady(projectDir, baseBranch, isPrivate = false) {
|
|
847
|
+
const skipCI = isPrivate ? ' [skip ci]' : '';
|
|
848
|
+
// Check if the repo has any commits at all
|
|
849
|
+
const hasCommits = git.execSafe('git rev-parse HEAD', projectDir);
|
|
850
|
+
if (!hasCommits.ok) {
|
|
851
|
+
// No commits yet — brand new repo
|
|
852
|
+
log.warn('No commits found in repository');
|
|
853
|
+
if (!git.isClean(projectDir)) {
|
|
854
|
+
log.info('Creating initial commit from existing code...');
|
|
855
|
+
git.commitAll(projectDir, `chore: initial commit${skipCI}`);
|
|
856
|
+
log.info('✅ Initial commit created');
|
|
857
|
+
} else {
|
|
858
|
+
log.warn('Repository is empty — no files to commit');
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
} else {
|
|
862
|
+
// Has commits, but base branch might have uncommitted changes
|
|
863
|
+
const currentBranch = git.currentBranch(projectDir);
|
|
864
|
+
if (currentBranch === baseBranch && !git.isClean(projectDir)) {
|
|
865
|
+
log.info('Uncommitted changes on base branch, committing first...');
|
|
866
|
+
git.commitAll(projectDir, `chore: save current progress before automation${skipCI}`);
|
|
867
|
+
log.info('✅ Base branch changes committed');
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Ensure we're on the base branch
|
|
872
|
+
const currentBranch = git.currentBranch(projectDir);
|
|
873
|
+
if (currentBranch !== baseBranch) {
|
|
874
|
+
// If base branch doesn't exist locally, create it from current
|
|
875
|
+
const branchExists = git.execSafe(`git rev-parse --verify ${baseBranch}`, projectDir);
|
|
876
|
+
if (!branchExists.ok) {
|
|
877
|
+
log.info(`Creating base branch '${baseBranch}' from current branch...`);
|
|
878
|
+
git.execSafe(`git branch ${baseBranch}`, projectDir);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Ensure base branch is pushed to remote
|
|
883
|
+
github.ensureRemoteBranch(projectDir, baseBranch);
|
|
884
|
+
log.info(`Base branch '${baseBranch}' ready ✓`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function buildDevPrompt(task, projectDir) {
|
|
888
|
+
const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
|
|
889
|
+
? task.acceptance.map(a => `- ${a}`).join('\n')
|
|
890
|
+
: '- Feature works correctly';
|
|
891
|
+
|
|
892
|
+
let retrySection = '';
|
|
893
|
+
if (projectDir) {
|
|
894
|
+
const retryContextPath = resolve(projectDir, `.codex-copilot/retry_context/${task.id}.md`);
|
|
895
|
+
if (existsSync(retryContextPath)) {
|
|
896
|
+
const retryContext = readFileSync(retryContextPath, 'utf-8');
|
|
897
|
+
retrySection = `\n## ⚠️ Retry Context (from previous failed attempt)\n${retryContext}\n`;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return `Please complete the following development task:
|
|
902
|
+
|
|
903
|
+
## Task #${task.id}: ${task.title}
|
|
904
|
+
|
|
905
|
+
${task.description}
|
|
906
|
+
${retrySection}
|
|
907
|
+
## Acceptance Criteria
|
|
908
|
+
${acceptanceList}
|
|
909
|
+
|
|
910
|
+
## Requirements
|
|
911
|
+
1. Strictly follow the project's existing code style and tech stack
|
|
912
|
+
2. Ensure the code compiles/runs correctly when done
|
|
913
|
+
3. Do NOT run git add or git commit — the automation handles committing
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function sleep(ms) {
|
|
918
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Wait for rate limit reset with countdown display.
|
|
923
|
+
* @param {Date} retryAt - When to resume
|
|
924
|
+
* @param {string} retryAtStr - Human-readable time string
|
|
925
|
+
*/
|
|
926
|
+
async function waitForRateLimitReset(retryAt, retryAtStr) {
|
|
927
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
928
|
+
let spinIdx = 0;
|
|
929
|
+
|
|
930
|
+
log.blank();
|
|
931
|
+
log.info(`⏳ Rate limited — auto-resuming at ${retryAtStr}`);
|
|
932
|
+
log.blank();
|
|
933
|
+
|
|
934
|
+
while (true) {
|
|
935
|
+
const now = Date.now();
|
|
936
|
+
const remaining = retryAt.getTime() - now;
|
|
937
|
+
if (remaining <= 0) break;
|
|
938
|
+
|
|
939
|
+
const mins = Math.floor(remaining / 60000);
|
|
940
|
+
const secs = Math.floor((remaining % 60000) / 1000);
|
|
941
|
+
const frame = SPINNER[spinIdx % SPINNER.length];
|
|
942
|
+
spinIdx++;
|
|
943
|
+
|
|
944
|
+
process.stdout.write(
|
|
945
|
+
`\r\x1b[K \x1b[33m${frame}\x1b[0m Waiting for rate limit reset... \x1b[1m${mins}m ${secs}s\x1b[0m remaining`
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
await sleep(1000);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
process.stdout.write('\r\x1b[K');
|
|
952
|
+
log.info('✅ Rate limit reset — waiting 3min buffer before resuming...');
|
|
953
|
+
// Add 3-minute buffer to ensure the limit is actually lifted
|
|
954
|
+
await sleep(180_000);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
|