@litmers/cursorflow-orchestrator 0.1.15 → 0.1.20
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/CHANGELOG.md +23 -1
- package/README.md +26 -7
- package/commands/cursorflow-run.md +2 -0
- package/commands/cursorflow-triggers.md +250 -0
- package/dist/cli/clean.js +8 -7
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +20 -14
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +64 -47
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +27 -17
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +73 -33
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +193 -40
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +3 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +7 -7
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +2 -1
- package/dist/core/orchestrator.js +54 -93
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +6 -4
- package/dist/core/reviewer.js +7 -5
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +8 -0
- package/dist/core/runner.js +219 -32
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +20 -10
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +35 -7
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +2 -2
- package/dist/utils/enhanced-logger.js +114 -43
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.js +163 -10
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +16 -0
- package/dist/utils/log-formatter.js +194 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/repro-thinking-logs.d.ts +1 -0
- package/dist/utils/repro-thinking-logs.js +80 -0
- package/dist/utils/repro-thinking-logs.js.map +1 -0
- package/dist/utils/state.d.ts +4 -1
- package/dist/utils/state.js +11 -8
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/webhook.js +3 -0
- package/dist/utils/webhook.js.map +1 -1
- package/package.json +4 -2
- package/scripts/ai-security-check.js +3 -0
- package/scripts/local-security-gate.sh +9 -1
- package/scripts/verify-and-fix.sh +37 -0
- package/src/cli/clean.ts +8 -7
- package/src/cli/index.ts +5 -1
- package/src/cli/init.ts +19 -15
- package/src/cli/logs.ts +67 -47
- package/src/cli/monitor.ts +28 -18
- package/src/cli/prepare.ts +75 -35
- package/src/cli/resume.ts +810 -626
- package/src/cli/run.ts +3 -2
- package/src/cli/signal.ts +7 -6
- package/src/core/orchestrator.ts +68 -93
- package/src/core/reviewer.ts +14 -9
- package/src/core/runner.ts +229 -33
- package/src/utils/config.ts +19 -11
- package/src/utils/doctor.ts +38 -7
- package/src/utils/enhanced-logger.ts +117 -49
- package/src/utils/git.ts +145 -11
- package/src/utils/log-formatter.ts +162 -0
- package/src/utils/path.ts +45 -0
- package/src/utils/repro-thinking-logs.ts +54 -0
- package/src/utils/state.ts +16 -8
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +13 -0
- package/src/utils/webhook.ts +3 -0
- package/templates/basic.json +21 -0
- package/scripts/simple-logging-test.sh +0 -97
- package/scripts/test-real-cursor-lifecycle.sh +0 -289
- package/scripts/test-real-logging.sh +0 -289
- package/scripts/test-streaming-multi-task.sh +0 -247
package/src/cli/resume.ts
CHANGED
|
@@ -1,626 +1,810 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CursorFlow resume command
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import { spawn, ChildProcess } from 'child_process';
|
|
8
|
-
import * as logger from '../utils/logger';
|
|
9
|
-
import { loadConfig, getLogsDir } from '../utils/config';
|
|
10
|
-
import { loadState
|
|
11
|
-
import { LaneState } from '../utils/types';
|
|
12
|
-
import { runDoctor } from '../utils/doctor';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
//
|
|
240
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
const
|
|
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
|
-
logger.
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* CursorFlow resume command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
8
|
+
import * as logger from '../utils/logger';
|
|
9
|
+
import { loadConfig, getLogsDir } from '../utils/config';
|
|
10
|
+
import { loadState } from '../utils/state';
|
|
11
|
+
import { LaneState } from '../utils/types';
|
|
12
|
+
import { runDoctor } from '../utils/doctor';
|
|
13
|
+
import { safeJoin } from '../utils/path';
|
|
14
|
+
import {
|
|
15
|
+
EnhancedLogManager,
|
|
16
|
+
createLogManager,
|
|
17
|
+
DEFAULT_LOG_CONFIG,
|
|
18
|
+
stripAnsi,
|
|
19
|
+
ParsedMessage
|
|
20
|
+
} from '../utils/enhanced-logger';
|
|
21
|
+
|
|
22
|
+
interface ResumeOptions {
|
|
23
|
+
lane: string | null;
|
|
24
|
+
runDir: string | null;
|
|
25
|
+
clean: boolean;
|
|
26
|
+
restart: boolean;
|
|
27
|
+
skipDoctor: boolean;
|
|
28
|
+
all: boolean;
|
|
29
|
+
status: boolean;
|
|
30
|
+
maxConcurrent: number;
|
|
31
|
+
help: boolean;
|
|
32
|
+
noGit: boolean;
|
|
33
|
+
executor: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printHelp(): void {
|
|
37
|
+
console.log(`
|
|
38
|
+
Usage: cursorflow resume [lane] [options]
|
|
39
|
+
|
|
40
|
+
Resume interrupted or failed lanes.
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
<lane> Lane name to resume (single lane mode)
|
|
44
|
+
--all Resume ALL incomplete/failed lanes
|
|
45
|
+
--status Show status of all lanes in the run (no resume)
|
|
46
|
+
--run-dir <path> Use a specific run directory (default: latest)
|
|
47
|
+
--max-concurrent <n> Max lanes to run in parallel (default: 3)
|
|
48
|
+
--clean Clean up existing worktree before resuming
|
|
49
|
+
--restart Restart from the first task (index 0)
|
|
50
|
+
--skip-doctor Skip environment/branch checks (not recommended)
|
|
51
|
+
--no-git Disable Git operations (must match original run)
|
|
52
|
+
--executor <type> Override executor (default: cursor-agent)
|
|
53
|
+
--help, -h Show help
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
cursorflow resume --status # Check status of all lanes
|
|
57
|
+
cursorflow resume --all # Resume all incomplete lanes
|
|
58
|
+
cursorflow resume lane-1 # Resume single lane
|
|
59
|
+
cursorflow resume --all --restart # Restart all incomplete lanes from task 0
|
|
60
|
+
cursorflow resume --all --max-concurrent 2 # Resume with max 2 parallel lanes
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseArgs(args: string[]): ResumeOptions {
|
|
65
|
+
const runDirIdx = args.indexOf('--run-dir');
|
|
66
|
+
const maxConcurrentIdx = args.indexOf('--max-concurrent');
|
|
67
|
+
const executorIdx = args.indexOf('--executor');
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
lane: args.find(a => !a.startsWith('--')) || null,
|
|
71
|
+
runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
|
|
72
|
+
clean: args.includes('--clean'),
|
|
73
|
+
restart: args.includes('--restart'),
|
|
74
|
+
skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
|
|
75
|
+
all: args.includes('--all'),
|
|
76
|
+
status: args.includes('--status'),
|
|
77
|
+
maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '3') : 3,
|
|
78
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
79
|
+
noGit: args.includes('--no-git'),
|
|
80
|
+
executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find the latest run directory
|
|
86
|
+
*/
|
|
87
|
+
function findLatestRunDir(logsDir: string): string | null {
|
|
88
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
89
|
+
if (!fs.existsSync(runsDir)) return null;
|
|
90
|
+
|
|
91
|
+
const runs = fs.readdirSync(runsDir)
|
|
92
|
+
.filter(d => d.startsWith('run-'))
|
|
93
|
+
.sort()
|
|
94
|
+
.reverse();
|
|
95
|
+
|
|
96
|
+
return runs.length > 0 ? safeJoin(runsDir, runs[0]!) : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Status indicator colors
|
|
101
|
+
*/
|
|
102
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
103
|
+
completed: '\x1b[32m', // green
|
|
104
|
+
running: '\x1b[36m', // cyan
|
|
105
|
+
pending: '\x1b[33m', // yellow
|
|
106
|
+
failed: '\x1b[31m', // red
|
|
107
|
+
paused: '\x1b[35m', // magenta
|
|
108
|
+
waiting: '\x1b[33m', // yellow
|
|
109
|
+
reviewing: '\x1b[36m', // cyan
|
|
110
|
+
unknown: '\x1b[90m', // gray
|
|
111
|
+
};
|
|
112
|
+
const RESET = '\x1b[0m';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format and print parsed message to console (copied from orchestrator.ts)
|
|
116
|
+
*/
|
|
117
|
+
function handleParsedMessage(laneName: string, msg: ParsedMessage): void {
|
|
118
|
+
const ts = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false });
|
|
119
|
+
const laneLabel = `[${laneName}]`.padEnd(12);
|
|
120
|
+
|
|
121
|
+
let prefix = '';
|
|
122
|
+
let content = msg.content;
|
|
123
|
+
|
|
124
|
+
switch (msg.type) {
|
|
125
|
+
case 'user':
|
|
126
|
+
prefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
|
|
127
|
+
content = content.replace(/\n/g, ' ');
|
|
128
|
+
break;
|
|
129
|
+
case 'assistant':
|
|
130
|
+
prefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
|
|
131
|
+
break;
|
|
132
|
+
case 'tool':
|
|
133
|
+
prefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
|
|
134
|
+
const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
|
|
135
|
+
if (toolMatch) {
|
|
136
|
+
const [, name, args] = toolMatch;
|
|
137
|
+
try {
|
|
138
|
+
const parsedArgs = JSON.parse(args!);
|
|
139
|
+
let argStr = '';
|
|
140
|
+
if (name === 'read_file' && parsedArgs.target_file) argStr = parsedArgs.target_file;
|
|
141
|
+
else if (name === 'run_terminal_cmd' && parsedArgs.command) argStr = parsedArgs.command;
|
|
142
|
+
else if (name === 'write' && parsedArgs.file_path) argStr = parsedArgs.file_path;
|
|
143
|
+
else if (name === 'search_replace' && parsedArgs.file_path) argStr = parsedArgs.file_path;
|
|
144
|
+
else {
|
|
145
|
+
const keys = Object.keys(parsedArgs);
|
|
146
|
+
if (keys.length > 0) argStr = String(parsedArgs[keys[0]]).substring(0, 50);
|
|
147
|
+
}
|
|
148
|
+
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
|
|
149
|
+
} catch {
|
|
150
|
+
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
case 'tool_result':
|
|
155
|
+
prefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
|
|
156
|
+
const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
|
|
157
|
+
content = resMatch ? `${resMatch[1]} OK` : 'result';
|
|
158
|
+
break;
|
|
159
|
+
case 'result':
|
|
160
|
+
prefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
|
|
161
|
+
break;
|
|
162
|
+
case 'system':
|
|
163
|
+
prefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
|
|
164
|
+
break;
|
|
165
|
+
case 'thinking':
|
|
166
|
+
prefix = `${logger.COLORS.gray}🤔 THNK${logger.COLORS.reset}`;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (prefix) {
|
|
171
|
+
const lines = content.split('\n');
|
|
172
|
+
const tsPrefix = `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneLabel}${logger.COLORS.reset}`;
|
|
173
|
+
|
|
174
|
+
if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'result' || msg.type === 'thinking') {
|
|
175
|
+
const header = `${prefix} ┌${'─'.repeat(60)}`;
|
|
176
|
+
process.stdout.write(`${tsPrefix} ${header}\n`);
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} │ ${line}\n`);
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} └${'─'.repeat(60)}\n`);
|
|
181
|
+
} else {
|
|
182
|
+
process.stdout.write(`${tsPrefix} ${prefix} ${content}\n`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface LaneInfo {
|
|
188
|
+
name: string;
|
|
189
|
+
dir: string;
|
|
190
|
+
state: LaneState | null;
|
|
191
|
+
needsResume: boolean;
|
|
192
|
+
dependsOn: string[];
|
|
193
|
+
isCompleted: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get all lane statuses from a run directory
|
|
198
|
+
*/
|
|
199
|
+
function getAllLaneStatuses(runDir: string): LaneInfo[] {
|
|
200
|
+
const lanesDir = safeJoin(runDir, 'lanes');
|
|
201
|
+
if (!fs.existsSync(lanesDir)) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const lanes = fs.readdirSync(lanesDir)
|
|
206
|
+
.filter(f => fs.statSync(safeJoin(lanesDir, f)).isDirectory())
|
|
207
|
+
.map(name => {
|
|
208
|
+
const dir = safeJoin(lanesDir, name);
|
|
209
|
+
const statePath = safeJoin(dir, 'state.json');
|
|
210
|
+
const state = fs.existsSync(statePath) ? loadState<LaneState>(statePath) : null;
|
|
211
|
+
|
|
212
|
+
// Determine if lane needs resume: everything that is not completed
|
|
213
|
+
const needsResume = state ? (
|
|
214
|
+
state.status !== 'completed'
|
|
215
|
+
) : true;
|
|
216
|
+
|
|
217
|
+
const isCompleted = state?.status === 'completed';
|
|
218
|
+
const dependsOn = state?.dependsOn || [];
|
|
219
|
+
|
|
220
|
+
return { name, dir, state, needsResume, dependsOn, isCompleted };
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return lanes;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if all dependencies of a lane are completed
|
|
228
|
+
*/
|
|
229
|
+
function areDependenciesCompleted(
|
|
230
|
+
lane: LaneInfo,
|
|
231
|
+
allLanes: LaneInfo[],
|
|
232
|
+
completedLanes: Set<string>
|
|
233
|
+
): boolean {
|
|
234
|
+
if (!lane.dependsOn || lane.dependsOn.length === 0) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const depName of lane.dependsOn) {
|
|
239
|
+
// Check if dependency is in completed set (already succeeded in this resume session)
|
|
240
|
+
if (completedLanes.has(depName)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check if dependency was already completed before this resume
|
|
245
|
+
const depLane = allLanes.find(l => l.name === depName);
|
|
246
|
+
if (!depLane || !depLane.isCompleted) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Print status of all lanes
|
|
256
|
+
*/
|
|
257
|
+
function printAllLaneStatus(runDir: string): { total: number; completed: number; needsResume: number } {
|
|
258
|
+
const lanes = getAllLaneStatuses(runDir);
|
|
259
|
+
|
|
260
|
+
if (lanes.length === 0) {
|
|
261
|
+
logger.warn('No lanes found in this run.');
|
|
262
|
+
return { total: 0, completed: 0, needsResume: 0 };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
logger.section(`📊 Lane Status (${path.basename(runDir)})`);
|
|
266
|
+
console.log('');
|
|
267
|
+
|
|
268
|
+
// Table header
|
|
269
|
+
console.log(' ' +
|
|
270
|
+
'Lane'.padEnd(25) +
|
|
271
|
+
'Status'.padEnd(12) +
|
|
272
|
+
'Progress'.padEnd(12) +
|
|
273
|
+
'DependsOn'.padEnd(15) +
|
|
274
|
+
'Resumable'
|
|
275
|
+
);
|
|
276
|
+
console.log(' ' + '-'.repeat(75));
|
|
277
|
+
|
|
278
|
+
let completedCount = 0;
|
|
279
|
+
let needsResumeCount = 0;
|
|
280
|
+
const completedSet = new Set<string>();
|
|
281
|
+
|
|
282
|
+
// First pass: collect completed lanes
|
|
283
|
+
for (const lane of lanes) {
|
|
284
|
+
if (lane.isCompleted) {
|
|
285
|
+
completedSet.add(lane.name);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const lane of lanes) {
|
|
290
|
+
const state = lane.state;
|
|
291
|
+
const status = state?.status || 'unknown';
|
|
292
|
+
const color = STATUS_COLORS[status] || STATUS_COLORS.unknown;
|
|
293
|
+
const progress = state ? `${state.currentTaskIndex}/${state.totalTasks}` : '-/-';
|
|
294
|
+
const dependsOnStr = lane.dependsOn.length > 0 ? lane.dependsOn.join(',').substring(0, 12) : '-';
|
|
295
|
+
|
|
296
|
+
// Check if dependencies are met
|
|
297
|
+
const depsCompleted = areDependenciesCompleted(lane, lanes, completedSet);
|
|
298
|
+
const canResume = lane.needsResume && depsCompleted;
|
|
299
|
+
const blockedByDep = lane.needsResume && !depsCompleted;
|
|
300
|
+
|
|
301
|
+
if (status === 'completed') completedCount++;
|
|
302
|
+
if (lane.needsResume) needsResumeCount++;
|
|
303
|
+
|
|
304
|
+
let resumeIndicator = '';
|
|
305
|
+
if (canResume) {
|
|
306
|
+
resumeIndicator = '\x1b[33m✓\x1b[0m';
|
|
307
|
+
} else if (blockedByDep) {
|
|
308
|
+
resumeIndicator = '\x1b[90m⏳ waiting\x1b[0m';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(' ' +
|
|
312
|
+
lane.name.padEnd(25) +
|
|
313
|
+
`${color}${status.padEnd(12)}${RESET}` +
|
|
314
|
+
progress.padEnd(12) +
|
|
315
|
+
dependsOnStr.padEnd(15) +
|
|
316
|
+
resumeIndicator
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Show error if failed
|
|
320
|
+
if (status === 'failed' && state?.error) {
|
|
321
|
+
console.log(` ${''.padEnd(25)}\x1b[31m└─ ${state.error.substring(0, 50)}${state.error.length > 50 ? '...' : ''}\x1b[0m`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Show blocked dependency info
|
|
325
|
+
if (blockedByDep) {
|
|
326
|
+
const pendingDeps = lane.dependsOn.filter(d => !completedSet.has(d));
|
|
327
|
+
console.log(` ${''.padEnd(25)}\x1b[90m└─ waiting for: ${pendingDeps.join(', ')}\x1b[0m`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log('');
|
|
332
|
+
console.log(` Total: ${lanes.length} | Completed: ${completedCount} | Needs Resume: ${needsResumeCount}`);
|
|
333
|
+
|
|
334
|
+
if (needsResumeCount > 0) {
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log(' \x1b[33mTip:\x1b[0m Run \x1b[32mcursorflow resume --all\x1b[0m to resume all incomplete lanes');
|
|
337
|
+
console.log(' Lanes with dependencies will wait until their dependencies complete.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { total: lanes.length, completed: completedCount, needsResume: needsResumeCount };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Resume a single lane and return the child process and its log manager
|
|
345
|
+
*/
|
|
346
|
+
function spawnLaneResume(
|
|
347
|
+
laneName: string,
|
|
348
|
+
laneDir: string,
|
|
349
|
+
state: LaneState,
|
|
350
|
+
options: {
|
|
351
|
+
restart: boolean;
|
|
352
|
+
noGit?: boolean;
|
|
353
|
+
pipelineBranch?: string;
|
|
354
|
+
executor?: string | null;
|
|
355
|
+
enhancedLogConfig?: any;
|
|
356
|
+
}
|
|
357
|
+
): { child: ChildProcess; logManager: EnhancedLogManager } {
|
|
358
|
+
const runnerPath = require.resolve('../core/runner');
|
|
359
|
+
const startIndex = options.restart ? 0 : state.currentTaskIndex;
|
|
360
|
+
|
|
361
|
+
const runnerArgs = [
|
|
362
|
+
runnerPath,
|
|
363
|
+
state.tasksFile!,
|
|
364
|
+
'--run-dir', laneDir,
|
|
365
|
+
'--start-index', String(startIndex),
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
if (state.worktreeDir) {
|
|
369
|
+
runnerArgs.push('--worktree-dir', state.worktreeDir);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (options.noGit) {
|
|
373
|
+
runnerArgs.push('--no-git');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Explicitly pass pipeline branch if available (either from state or override)
|
|
377
|
+
const branch = options.pipelineBranch || state.pipelineBranch;
|
|
378
|
+
if (branch) {
|
|
379
|
+
runnerArgs.push('--pipeline-branch', branch);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Pass executor if provided
|
|
383
|
+
if (options.executor) {
|
|
384
|
+
runnerArgs.push('--executor', options.executor);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const logManager = createLogManager(laneDir, laneName, options.enhancedLogConfig || {}, (msg) => handleParsedMessage(laneName, msg));
|
|
388
|
+
|
|
389
|
+
const child = spawn('node', runnerArgs, {
|
|
390
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
391
|
+
env: process.env,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
let lineBuffer = '';
|
|
395
|
+
|
|
396
|
+
if (child.stdout) {
|
|
397
|
+
child.stdout.on('data', (data: Buffer) => {
|
|
398
|
+
logManager.writeStdout(data);
|
|
399
|
+
|
|
400
|
+
const str = data.toString();
|
|
401
|
+
lineBuffer += str;
|
|
402
|
+
const lines = lineBuffer.split('\n');
|
|
403
|
+
lineBuffer = lines.pop() || '';
|
|
404
|
+
|
|
405
|
+
for (const line of lines) {
|
|
406
|
+
const trimmed = line.trim();
|
|
407
|
+
if (trimmed &&
|
|
408
|
+
!trimmed.startsWith('{') &&
|
|
409
|
+
!trimmed.startsWith('[') &&
|
|
410
|
+
!trimmed.includes('{"type"')) {
|
|
411
|
+
process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneName.padEnd(10)}${logger.COLORS.reset} ${line}\n`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (child.stderr) {
|
|
418
|
+
child.stderr.on('data', (data: Buffer) => {
|
|
419
|
+
logManager.writeStderr(data);
|
|
420
|
+
const str = data.toString();
|
|
421
|
+
const lines = str.split('\n');
|
|
422
|
+
for (const line of lines) {
|
|
423
|
+
const trimmed = line.trim();
|
|
424
|
+
if (trimmed) {
|
|
425
|
+
const isStatus = trimmed.startsWith('Preparing worktree') ||
|
|
426
|
+
trimmed.startsWith('Switched to a new branch') ||
|
|
427
|
+
trimmed.startsWith('HEAD is now at') ||
|
|
428
|
+
trimmed.includes('actual output');
|
|
429
|
+
|
|
430
|
+
if (isStatus) {
|
|
431
|
+
process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneName.padEnd(10)}${logger.COLORS.reset} ${trimmed}\n`);
|
|
432
|
+
} else {
|
|
433
|
+
process.stderr.write(`${logger.COLORS.red}[${laneName}] ERROR: ${trimmed}${logger.COLORS.reset}\n`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
child.on('exit', () => {
|
|
441
|
+
logManager.close();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
return { child, logManager };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Wait for a child process to exit
|
|
449
|
+
*/
|
|
450
|
+
function waitForChild(child: ChildProcess): Promise<number> {
|
|
451
|
+
return new Promise((resolve, reject) => {
|
|
452
|
+
child.on('exit', (code) => {
|
|
453
|
+
resolve(code ?? -1);
|
|
454
|
+
});
|
|
455
|
+
child.on('error', (err) => {
|
|
456
|
+
reject(err);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Resume multiple lanes with concurrency control and dependency awareness
|
|
463
|
+
*/
|
|
464
|
+
async function resumeAllLanes(
|
|
465
|
+
runDir: string,
|
|
466
|
+
options: {
|
|
467
|
+
restart: boolean;
|
|
468
|
+
maxConcurrent: number;
|
|
469
|
+
skipDoctor: boolean;
|
|
470
|
+
noGit: boolean;
|
|
471
|
+
executor: string | null;
|
|
472
|
+
enhancedLogConfig?: any;
|
|
473
|
+
}
|
|
474
|
+
): Promise<{ succeeded: string[]; failed: string[]; skipped: string[] }> {
|
|
475
|
+
const allLanes = getAllLaneStatuses(runDir);
|
|
476
|
+
const lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile);
|
|
477
|
+
const missingTaskInfo = allLanes.filter(l => l.needsResume && !l.state?.tasksFile);
|
|
478
|
+
|
|
479
|
+
if (missingTaskInfo.length > 0) {
|
|
480
|
+
logger.warn(`Lanes that haven't started yet and have no task info: ${missingTaskInfo.map(l => l.name).join(', ')}`);
|
|
481
|
+
logger.warn('These lanes cannot be resumed because their original task file paths were not recorded.');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (lanesToResume.length === 0) {
|
|
485
|
+
logger.success('All lanes are already completed! Nothing to resume.');
|
|
486
|
+
return { succeeded: [], failed: [], skipped: [] };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Check for lanes with unmet dependencies that can never be satisfied
|
|
490
|
+
const completedSet = new Set<string>(allLanes.filter(l => l.isCompleted).map(l => l.name));
|
|
491
|
+
const toResumeNames = new Set<string>(lanesToResume.map(l => l.name));
|
|
492
|
+
|
|
493
|
+
const skippedLanes: string[] = [];
|
|
494
|
+
const resolvableLanes: LaneInfo[] = [];
|
|
495
|
+
|
|
496
|
+
for (const lane of lanesToResume) {
|
|
497
|
+
// Check if all dependencies can be satisfied (either already completed or in the resume list)
|
|
498
|
+
const unmetDeps = lane.dependsOn.filter(dep =>
|
|
499
|
+
!completedSet.has(dep) && !toResumeNames.has(dep)
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
if (unmetDeps.length > 0) {
|
|
503
|
+
logger.warn(`⏭ Skipping ${lane.name}: unresolvable dependencies (${unmetDeps.join(', ')})`);
|
|
504
|
+
skippedLanes.push(lane.name);
|
|
505
|
+
} else {
|
|
506
|
+
resolvableLanes.push(lane);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (resolvableLanes.length === 0) {
|
|
511
|
+
logger.warn('No lanes can be resumed due to dependency constraints.');
|
|
512
|
+
return { succeeded: [], failed: [], skipped: skippedLanes };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
logger.section(`🔁 Resuming ${resolvableLanes.length} Lane(s)`);
|
|
516
|
+
logger.info(`Max concurrent: ${options.maxConcurrent}`);
|
|
517
|
+
logger.info(`Mode: ${options.restart ? 'Restart from beginning' : 'Continue from last task'}`);
|
|
518
|
+
|
|
519
|
+
// Show dependency order
|
|
520
|
+
const lanesWithDeps = resolvableLanes.filter(l => l.dependsOn.length > 0);
|
|
521
|
+
if (lanesWithDeps.length > 0) {
|
|
522
|
+
logger.info(`Dependency-aware: ${lanesWithDeps.length} lane(s) have dependencies`);
|
|
523
|
+
}
|
|
524
|
+
console.log('');
|
|
525
|
+
|
|
526
|
+
// Run doctor check once if needed (check git status)
|
|
527
|
+
if (!options.skipDoctor) {
|
|
528
|
+
logger.info('Running pre-flight checks...');
|
|
529
|
+
|
|
530
|
+
// Use the first lane's tasksDir for doctor check
|
|
531
|
+
const firstLane = resolvableLanes[0]!;
|
|
532
|
+
const tasksDir = path.dirname(firstLane.state!.tasksFile!);
|
|
533
|
+
|
|
534
|
+
const report = runDoctor({
|
|
535
|
+
cwd: process.cwd(),
|
|
536
|
+
tasksDir,
|
|
537
|
+
includeCursorAgentChecks: false,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const blockingIssues = report.issues.filter(i =>
|
|
541
|
+
i.severity === 'error' &&
|
|
542
|
+
(i.id.startsWith('branch.') || i.id.startsWith('git.'))
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
if (blockingIssues.length > 0) {
|
|
546
|
+
logger.section('🛑 Pre-resume check found issues');
|
|
547
|
+
for (const issue of blockingIssues) {
|
|
548
|
+
logger.error(`${issue.title} (${issue.id})`, '❌');
|
|
549
|
+
console.log(` ${issue.message}`);
|
|
550
|
+
}
|
|
551
|
+
throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass.');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const succeeded: string[] = [];
|
|
556
|
+
const failed: string[] = [];
|
|
557
|
+
|
|
558
|
+
// Create a mutable set for tracking completed lanes (including those from this session)
|
|
559
|
+
const sessionCompleted = new Set<string>(completedSet);
|
|
560
|
+
|
|
561
|
+
// Queue management with dependency awareness
|
|
562
|
+
const pending = new Set<string>(resolvableLanes.map(l => l.name));
|
|
563
|
+
const active: Map<string, ChildProcess> = new Map();
|
|
564
|
+
const laneMap = new Map<string, LaneInfo>(resolvableLanes.map(l => [l.name, l]));
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Find the next lane that can be started (all dependencies met)
|
|
568
|
+
*/
|
|
569
|
+
const findReadyLane = (): LaneInfo | null => {
|
|
570
|
+
for (const laneName of pending) {
|
|
571
|
+
const lane = laneMap.get(laneName)!;
|
|
572
|
+
if (areDependenciesCompleted(lane, allLanes, sessionCompleted)) {
|
|
573
|
+
return lane;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Process lanes with dependency awareness
|
|
581
|
+
*/
|
|
582
|
+
const processNext = (): void => {
|
|
583
|
+
while (active.size < options.maxConcurrent) {
|
|
584
|
+
const lane = findReadyLane();
|
|
585
|
+
|
|
586
|
+
if (!lane) {
|
|
587
|
+
// No lane ready to start
|
|
588
|
+
if (pending.size > 0 && active.size === 0) {
|
|
589
|
+
// Deadlock: pending lanes exist but none can start and none are running
|
|
590
|
+
const pendingList = Array.from(pending).join(', ');
|
|
591
|
+
logger.error(`Deadlock detected! Lanes waiting: ${pendingList}`);
|
|
592
|
+
for (const ln of pending) {
|
|
593
|
+
failed.push(ln);
|
|
594
|
+
}
|
|
595
|
+
pending.clear();
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
pending.delete(lane.name);
|
|
601
|
+
|
|
602
|
+
const depsInfo = lane.dependsOn.length > 0 ? ` (after: ${lane.dependsOn.join(', ')})` : '';
|
|
603
|
+
logger.info(`Starting: ${lane.name} (task ${lane.state!.currentTaskIndex}/${lane.state!.totalTasks})${depsInfo}`);
|
|
604
|
+
|
|
605
|
+
const { child } = spawnLaneResume(lane.name, lane.dir, lane.state!, {
|
|
606
|
+
restart: options.restart,
|
|
607
|
+
noGit: options.noGit,
|
|
608
|
+
executor: options.executor,
|
|
609
|
+
enhancedLogConfig: options.enhancedLogConfig,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
active.set(lane.name, child);
|
|
613
|
+
|
|
614
|
+
// Handle completion
|
|
615
|
+
waitForChild(child).then(code => {
|
|
616
|
+
active.delete(lane.name);
|
|
617
|
+
|
|
618
|
+
if (code === 0) {
|
|
619
|
+
logger.success(`✓ ${lane.name} completed`);
|
|
620
|
+
succeeded.push(lane.name);
|
|
621
|
+
sessionCompleted.add(lane.name); // Mark as completed for dependency resolution
|
|
622
|
+
} else if (code === 2) {
|
|
623
|
+
logger.warn(`⚠ ${lane.name} blocked on dependency change`);
|
|
624
|
+
failed.push(lane.name);
|
|
625
|
+
} else {
|
|
626
|
+
logger.error(`✗ ${lane.name} failed (exit ${code})`);
|
|
627
|
+
failed.push(lane.name);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Try to start more lanes now that one completed
|
|
631
|
+
processNext();
|
|
632
|
+
}).catch(err => {
|
|
633
|
+
active.delete(lane.name);
|
|
634
|
+
logger.error(`✗ ${lane.name} error: ${err.message}`);
|
|
635
|
+
failed.push(lane.name);
|
|
636
|
+
processNext();
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// Start initial batch
|
|
642
|
+
processNext();
|
|
643
|
+
|
|
644
|
+
// Wait for all to complete
|
|
645
|
+
while (active.size > 0 || pending.size > 0) {
|
|
646
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
647
|
+
|
|
648
|
+
// Check if we can start more (in case completion handlers haven't triggered processNext yet)
|
|
649
|
+
if (active.size < options.maxConcurrent && pending.size > 0) {
|
|
650
|
+
processNext();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Summary
|
|
655
|
+
console.log('');
|
|
656
|
+
logger.section('📊 Resume Summary');
|
|
657
|
+
logger.info(`Succeeded: ${succeeded.length}`);
|
|
658
|
+
if (failed.length > 0) {
|
|
659
|
+
logger.error(`Failed: ${failed.length} (${failed.join(', ')})`);
|
|
660
|
+
}
|
|
661
|
+
if (skippedLanes.length > 0) {
|
|
662
|
+
logger.warn(`Skipped: ${skippedLanes.length} (${skippedLanes.join(', ')})`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return { succeeded, failed, skipped: skippedLanes };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function resume(args: string[]): Promise<void> {
|
|
669
|
+
const options = parseArgs(args);
|
|
670
|
+
|
|
671
|
+
if (options.help) {
|
|
672
|
+
printHelp();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const config = loadConfig();
|
|
677
|
+
const logsDir = getLogsDir(config);
|
|
678
|
+
|
|
679
|
+
// Find run directory
|
|
680
|
+
let runDir = options.runDir;
|
|
681
|
+
if (!runDir) {
|
|
682
|
+
runDir = findLatestRunDir(logsDir);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!runDir || !fs.existsSync(runDir)) {
|
|
686
|
+
throw new Error(`Run directory not found: ${runDir || 'latest'}. Have you run any tasks yet?`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Status mode: just show status and exit
|
|
690
|
+
if (options.status) {
|
|
691
|
+
printAllLaneStatus(runDir);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// All mode: resume all incomplete lanes
|
|
696
|
+
if (options.all) {
|
|
697
|
+
const result = await resumeAllLanes(runDir, {
|
|
698
|
+
restart: options.restart,
|
|
699
|
+
maxConcurrent: options.maxConcurrent,
|
|
700
|
+
skipDoctor: options.skipDoctor,
|
|
701
|
+
noGit: options.noGit,
|
|
702
|
+
executor: options.executor,
|
|
703
|
+
enhancedLogConfig: config.enhancedLogging,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
if (result.failed.length > 0) {
|
|
707
|
+
throw new Error(`${result.failed.length} lane(s) failed to complete`);
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Single lane mode (original behavior)
|
|
713
|
+
if (!options.lane) {
|
|
714
|
+
// Show status by default if no lane specified
|
|
715
|
+
printAllLaneStatus(runDir);
|
|
716
|
+
console.log('');
|
|
717
|
+
console.log('Usage: cursorflow resume <lane> [options]');
|
|
718
|
+
console.log(' cursorflow resume --all # Resume all incomplete lanes');
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const laneDir = safeJoin(runDir, 'lanes', options.lane);
|
|
723
|
+
const statePath = safeJoin(laneDir, 'state.json');
|
|
724
|
+
|
|
725
|
+
if (!fs.existsSync(statePath)) {
|
|
726
|
+
throw new Error(`Lane state not found at ${statePath}. Is the lane name correct?`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const state = loadState<LaneState>(statePath);
|
|
730
|
+
if (!state) {
|
|
731
|
+
throw new Error(`Failed to load state from ${statePath}`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!state.tasksFile || !fs.existsSync(state.tasksFile)) {
|
|
735
|
+
throw new Error(`Original tasks file not found: ${state.tasksFile}. Resume impossible without task definition.`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Run doctor check before resuming (check branches, etc.)
|
|
739
|
+
if (!options.skipDoctor) {
|
|
740
|
+
const tasksDir = path.dirname(state.tasksFile);
|
|
741
|
+
logger.info('Running pre-flight checks...');
|
|
742
|
+
|
|
743
|
+
const report = runDoctor({
|
|
744
|
+
cwd: process.cwd(),
|
|
745
|
+
tasksDir,
|
|
746
|
+
includeCursorAgentChecks: false, // Skip agent checks for resume
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Only show blocking errors for resume
|
|
750
|
+
const blockingIssues = report.issues.filter(i =>
|
|
751
|
+
i.severity === 'error' &&
|
|
752
|
+
(i.id.startsWith('branch.') || i.id.startsWith('git.'))
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
if (blockingIssues.length > 0) {
|
|
756
|
+
logger.section('🛑 Pre-resume check found issues');
|
|
757
|
+
for (const issue of blockingIssues) {
|
|
758
|
+
logger.error(`${issue.title} (${issue.id})`, '❌');
|
|
759
|
+
console.log(` ${issue.message}`);
|
|
760
|
+
if (issue.details) console.log(` Details: ${issue.details}`);
|
|
761
|
+
if (issue.fixes?.length) {
|
|
762
|
+
console.log(' Fix:');
|
|
763
|
+
for (const fix of issue.fixes) console.log(` - ${fix}`);
|
|
764
|
+
}
|
|
765
|
+
console.log('');
|
|
766
|
+
}
|
|
767
|
+
throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass (not recommended).');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Show warnings but don't block
|
|
771
|
+
const warnings = report.issues.filter(i => i.severity === 'warn' && i.id.startsWith('branch.'));
|
|
772
|
+
if (warnings.length > 0) {
|
|
773
|
+
logger.warn(`${warnings.length} warning(s) found. Run 'cursorflow doctor' for details.`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
logger.section(`🔁 Resuming Lane: ${options.lane}`);
|
|
778
|
+
logger.info(`Run: ${path.basename(runDir)}`);
|
|
779
|
+
logger.info(`Tasks: ${state.tasksFile}`);
|
|
780
|
+
logger.info(`Starting from task index: ${options.restart ? 0 : state.currentTaskIndex}`);
|
|
781
|
+
|
|
782
|
+
const { child } = spawnLaneResume(options.lane, laneDir, state, {
|
|
783
|
+
restart: options.restart,
|
|
784
|
+
noGit: options.noGit,
|
|
785
|
+
executor: options.executor,
|
|
786
|
+
enhancedLogConfig: config.enhancedLogging,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
logger.info(`Spawning runner process...`);
|
|
790
|
+
|
|
791
|
+
return new Promise((resolve, reject) => {
|
|
792
|
+
child.on('exit', (code) => {
|
|
793
|
+
if (code === 0) {
|
|
794
|
+
logger.success(`Lane ${options.lane} completed successfully`);
|
|
795
|
+
resolve();
|
|
796
|
+
} else if (code === 2) {
|
|
797
|
+
logger.warn(`Lane ${options.lane} blocked on dependency change`);
|
|
798
|
+
resolve();
|
|
799
|
+
} else {
|
|
800
|
+
reject(new Error(`Lane ${options.lane} failed with exit code ${code}`));
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
child.on('error', (error) => {
|
|
805
|
+
reject(new Error(`Failed to start runner: ${error.message}`));
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export = resume;
|