@litmers/cursorflow-orchestrator 0.1.37 → 0.1.39
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/commands/cursorflow-init.md +113 -32
- package/commands/cursorflow-prepare.md +146 -339
- package/commands/cursorflow-run.md +148 -131
- package/dist/cli/add.js +8 -4
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/new.js +3 -5
- package/dist/cli/new.js.map +1 -1
- package/dist/cli/resume.js +26 -15
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +1 -6
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/setup-commands.d.ts +1 -0
- package/dist/cli/setup-commands.js +1 -0
- package/dist/cli/setup-commands.js.map +1 -1
- package/dist/core/runner/agent.d.ts +5 -1
- package/dist/core/runner/agent.js +31 -1
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/core/runner/pipeline.d.ts +0 -1
- package/dist/core/runner/pipeline.js +116 -167
- package/dist/core/runner/pipeline.js.map +1 -1
- package/dist/core/runner/prompt.d.ts +0 -1
- package/dist/core/runner/prompt.js +11 -16
- package/dist/core/runner/prompt.js.map +1 -1
- package/dist/core/runner/task.d.ts +1 -2
- package/dist/core/runner/task.js +24 -36
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +12 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +16 -4
- package/dist/core/stall-detection.js +97 -148
- package/dist/core/stall-detection.js.map +1 -1
- package/dist/services/logging/console.d.ts +7 -1
- package/dist/services/logging/console.js +15 -3
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.js +2 -0
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/types/config.d.ts +1 -1
- package/dist/types/logging.d.ts +1 -1
- package/dist/types/task.d.ts +2 -7
- package/dist/utils/doctor.js +4 -4
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/git.js +2 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.js +13 -13
- package/dist/utils/health.js.map +1 -1
- package/dist/utils/log-formatter.js +44 -7
- package/dist/utils/log-formatter.js.map +1 -1
- package/dist/utils/logger.js +2 -2
- package/dist/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +9 -4
- package/src/cli/index.ts +3 -0
- package/src/cli/new.ts +3 -5
- package/src/cli/resume.ts +30 -19
- package/src/cli/run.ts +1 -6
- package/src/cli/setup-commands.ts +1 -1
- package/src/core/runner/agent.ts +36 -4
- package/src/core/runner/pipeline.ts +127 -176
- package/src/core/runner/prompt.ts +11 -18
- package/src/core/runner/task.ts +24 -37
- package/src/core/runner.ts +13 -2
- package/src/core/stall-detection.ts +190 -146
- package/src/services/logging/console.ts +15 -3
- package/src/services/logging/formatter.ts +2 -0
- package/src/types/config.ts +1 -1
- package/src/types/logging.ts +4 -2
- package/src/types/task.ts +2 -7
- package/src/utils/doctor.ts +5 -5
- package/src/utils/git.ts +2 -0
- package/src/utils/health.ts +15 -15
- package/src/utils/log-formatter.ts +50 -7
- package/src/utils/logger.ts +2 -2
- package/commands/cursorflow-add.md +0 -159
- package/commands/cursorflow-clean.md +0 -84
- package/commands/cursorflow-doctor.md +0 -102
- package/commands/cursorflow-models.md +0 -51
- package/commands/cursorflow-monitor.md +0 -90
- package/commands/cursorflow-new.md +0 -87
- package/commands/cursorflow-resume.md +0 -205
- package/commands/cursorflow-signal.md +0 -52
- package/commands/cursorflow-stop.md +0 -55
- package/commands/cursorflow-triggers.md +0 -250
|
@@ -201,6 +201,21 @@ export interface FailureRecord {
|
|
|
201
201
|
lastOutput: string;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// 분석 컨텍스트 (Analysis Context)
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/** 분석에 필요한 시간 및 상태 컨텍스트 */
|
|
209
|
+
interface AnalysisContext {
|
|
210
|
+
state: LaneStallState;
|
|
211
|
+
idleTime: number;
|
|
212
|
+
progressTime: number;
|
|
213
|
+
taskTime: number;
|
|
214
|
+
timeSincePhaseChange: number;
|
|
215
|
+
bytesDelta: number;
|
|
216
|
+
effectiveIdleTimeout: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
204
219
|
// ============================================================================
|
|
205
220
|
// Stall Detection Service
|
|
206
221
|
// ============================================================================
|
|
@@ -420,207 +435,236 @@ export class StallDetectionService {
|
|
|
420
435
|
// Stall 분석 (Analysis)
|
|
421
436
|
// --------------------------------------------------------------------------
|
|
422
437
|
|
|
438
|
+
/** StallAnalysis 생성 헬퍼 */
|
|
439
|
+
private buildAnalysis(
|
|
440
|
+
type: StallType,
|
|
441
|
+
action: RecoveryAction,
|
|
442
|
+
message: string,
|
|
443
|
+
isTransient: boolean,
|
|
444
|
+
details?: Record<string, any>
|
|
445
|
+
): StallAnalysis {
|
|
446
|
+
return { type, action, message, isTransient, details };
|
|
447
|
+
}
|
|
448
|
+
|
|
423
449
|
/**
|
|
424
450
|
* Stall 상태 분석 - 현재 상태에서 필요한 액션 결정
|
|
425
451
|
*
|
|
426
452
|
* 분석 우선순위:
|
|
427
453
|
* 1. Task timeout (30분) → RESTART/DOCTOR
|
|
428
|
-
* 2. Zero bytes + idle →
|
|
454
|
+
* 2. Zero bytes + idle → phase별 에스컬레이션
|
|
429
455
|
* 3. No progress (10분) → 단계별 에스컬레이션
|
|
430
456
|
* 4. Idle timeout (2분) → 단계별 에스컬레이션
|
|
431
457
|
*/
|
|
432
458
|
analyzeStall(laneName: string): StallAnalysis {
|
|
433
459
|
const state = this.laneStates.get(laneName);
|
|
434
460
|
if (!state) {
|
|
435
|
-
return
|
|
436
|
-
type: StallType.IDLE,
|
|
437
|
-
action: RecoveryAction.NONE,
|
|
438
|
-
message: 'Lane not found',
|
|
439
|
-
isTransient: false,
|
|
440
|
-
};
|
|
461
|
+
return this.buildAnalysis(StallType.IDLE, RecoveryAction.NONE, 'Lane not found', false);
|
|
441
462
|
}
|
|
442
463
|
|
|
443
|
-
const
|
|
444
|
-
const idleTime = now - state.lastRealActivityTime;
|
|
445
|
-
const progressTime = now - state.lastStateUpdateTime;
|
|
446
|
-
const taskTime = now - state.taskStartTime;
|
|
447
|
-
const timeSincePhaseChange = now - state.lastPhaseChangeTime;
|
|
464
|
+
const ctx = this.buildAnalysisContext(state);
|
|
448
465
|
|
|
449
|
-
//
|
|
450
|
-
const
|
|
466
|
+
// 1. Task timeout (최우선)
|
|
467
|
+
const taskResult = this.checkTaskTimeout(ctx);
|
|
468
|
+
if (taskResult) return taskResult;
|
|
451
469
|
|
|
452
|
-
//
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
: this.config.idleTimeoutMs;
|
|
470
|
+
// 2. Zero bytes + idle (에이전트 무응답)
|
|
471
|
+
const zeroByteResult = this.checkZeroBytes(ctx);
|
|
472
|
+
if (zeroByteResult) return zeroByteResult;
|
|
456
473
|
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
type: StallType.TASK_TIMEOUT,
|
|
461
|
-
action: state.restartCount < this.config.maxRestarts
|
|
462
|
-
? RecoveryAction.REQUEST_RESTART
|
|
463
|
-
: RecoveryAction.RUN_DOCTOR,
|
|
464
|
-
message: `Task exceeded maximum timeout of ${Math.round(this.config.taskTimeoutMs / 60000)} minutes`,
|
|
465
|
-
isTransient: state.restartCount < this.config.maxRestarts,
|
|
466
|
-
details: { taskTimeMs: taskTime, restartCount: state.restartCount },
|
|
467
|
-
};
|
|
468
|
-
}
|
|
474
|
+
// 3. Progress timeout
|
|
475
|
+
const progressResult = this.checkProgressTimeout(ctx);
|
|
476
|
+
if (progressResult) return progressResult;
|
|
469
477
|
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
478
|
+
// 4. Phase별 idle 체크
|
|
479
|
+
const idleResult = this.checkPhaseBasedIdle(ctx);
|
|
480
|
+
if (idleResult) return idleResult;
|
|
481
|
+
|
|
482
|
+
// 액션 필요 없음
|
|
483
|
+
return this.buildAnalysis(StallType.IDLE, RecoveryAction.NONE, 'Monitoring', true);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** 분석 컨텍스트 생성 */
|
|
487
|
+
private buildAnalysisContext(state: LaneStallState): AnalysisContext {
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
return {
|
|
490
|
+
state,
|
|
491
|
+
idleTime: now - state.lastRealActivityTime,
|
|
492
|
+
progressTime: now - state.lastStateUpdateTime,
|
|
493
|
+
taskTime: now - state.taskStartTime,
|
|
494
|
+
timeSincePhaseChange: now - state.lastPhaseChangeTime,
|
|
495
|
+
bytesDelta: state.totalBytesReceived - state.bytesAtLastCheck,
|
|
496
|
+
effectiveIdleTimeout: state.isLongOperation
|
|
497
|
+
? this.config.longOperationGraceMs
|
|
498
|
+
: this.config.idleTimeoutMs,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Task timeout 체크 */
|
|
503
|
+
private checkTaskTimeout(ctx: AnalysisContext): StallAnalysis | null {
|
|
504
|
+
if (ctx.taskTime <= this.config.taskTimeoutMs) return null;
|
|
505
|
+
|
|
506
|
+
const canRestart = ctx.state.restartCount < this.config.maxRestarts;
|
|
507
|
+
return this.buildAnalysis(
|
|
508
|
+
StallType.TASK_TIMEOUT,
|
|
509
|
+
canRestart ? RecoveryAction.REQUEST_RESTART : RecoveryAction.RUN_DOCTOR,
|
|
510
|
+
`Task exceeded maximum timeout of ${Math.round(this.config.taskTimeoutMs / 60000)} minutes`,
|
|
511
|
+
canRestart,
|
|
512
|
+
{ taskTimeMs: ctx.taskTime, restartCount: ctx.state.restartCount }
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Zero bytes 체크 (grace period 존중) */
|
|
517
|
+
private checkZeroBytes(ctx: AnalysisContext): StallAnalysis | null {
|
|
518
|
+
const { state, idleTime, timeSincePhaseChange, bytesDelta, effectiveIdleTimeout } = ctx;
|
|
519
|
+
|
|
520
|
+
if (bytesDelta !== 0 || idleTime <= effectiveIdleTimeout) return null;
|
|
521
|
+
|
|
522
|
+
const baseDetails = { idleTimeMs: idleTime, bytesDelta, phase: state.phase, timeSincePhaseChange };
|
|
482
523
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
524
|
+
switch (state.phase) {
|
|
525
|
+
case StallPhase.NORMAL:
|
|
526
|
+
return this.buildAnalysis(
|
|
527
|
+
StallType.ZERO_BYTES,
|
|
528
|
+
RecoveryAction.SEND_CONTINUE,
|
|
529
|
+
`Agent produced 0 bytes for ${Math.round(idleTime / 1000)}s - possible API issue`,
|
|
530
|
+
true, baseDetails
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
case StallPhase.CONTINUE_SENT:
|
|
534
|
+
if (timeSincePhaseChange > this.config.continueGraceMs) {
|
|
535
|
+
return this.buildAnalysis(
|
|
536
|
+
StallType.ZERO_BYTES,
|
|
537
|
+
RecoveryAction.SEND_STRONGER_PROMPT,
|
|
538
|
+
`Still 0 bytes after continue signal (${Math.round(timeSincePhaseChange / 1000)}s). Escalating...`,
|
|
539
|
+
true, baseDetails
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return null; // Grace period 내 → 대기
|
|
543
|
+
|
|
544
|
+
case StallPhase.STRONGER_PROMPT_SENT:
|
|
545
|
+
if (timeSincePhaseChange > this.config.strongerPromptGraceMs) {
|
|
546
|
+
return this.buildAnalysis(
|
|
547
|
+
StallType.ZERO_BYTES,
|
|
548
|
+
RecoveryAction.REQUEST_RESTART,
|
|
549
|
+
`Still 0 bytes after stronger prompt (${Math.round(timeSincePhaseChange / 1000)}s). Restarting...`,
|
|
550
|
+
true, baseDetails
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return null; // Grace period 내 → 대기
|
|
554
|
+
|
|
555
|
+
default:
|
|
556
|
+
// RESTART_REQUESTED, DIAGNOSED, ABORTED
|
|
557
|
+
return this.buildAnalysis(
|
|
558
|
+
StallType.ZERO_BYTES,
|
|
559
|
+
RecoveryAction.REQUEST_RESTART,
|
|
560
|
+
`Agent produced 0 bytes for ${Math.round(idleTime / 1000)}s - possible API issue`,
|
|
561
|
+
true, baseDetails
|
|
562
|
+
);
|
|
486
563
|
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Progress timeout 체크 */
|
|
567
|
+
private checkProgressTimeout(ctx: AnalysisContext): StallAnalysis | null {
|
|
568
|
+
if (ctx.progressTime <= this.config.progressTimeoutMs) return null;
|
|
569
|
+
return this.getEscalatedAction(ctx.state, StallType.NO_PROGRESS, ctx.progressTime);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Phase별 idle 상태 체크 */
|
|
573
|
+
private checkPhaseBasedIdle(ctx: AnalysisContext): StallAnalysis | null {
|
|
574
|
+
const { state, idleTime, timeSincePhaseChange, effectiveIdleTimeout } = ctx;
|
|
487
575
|
|
|
488
|
-
// 4. Phase별 상태 체크
|
|
489
576
|
switch (state.phase) {
|
|
490
577
|
case StallPhase.NORMAL:
|
|
491
|
-
// Idle timeout 체크
|
|
492
578
|
if (idleTime > effectiveIdleTimeout) {
|
|
493
|
-
return
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
579
|
+
return this.buildAnalysis(
|
|
580
|
+
StallType.IDLE,
|
|
581
|
+
RecoveryAction.SEND_CONTINUE,
|
|
582
|
+
`Lane idle for ${Math.round(idleTime / 1000)}s. Sending continue signal...`,
|
|
583
|
+
true,
|
|
584
|
+
{ idleTimeMs: idleTime, isLongOperation: state.isLongOperation }
|
|
585
|
+
);
|
|
500
586
|
}
|
|
501
587
|
break;
|
|
502
588
|
|
|
503
589
|
case StallPhase.CONTINUE_SENT:
|
|
504
|
-
// Continue 신호 후 유예 시간 초과
|
|
505
590
|
if (timeSincePhaseChange > this.config.continueGraceMs) {
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
591
|
+
return this.buildAnalysis(
|
|
592
|
+
StallType.IDLE,
|
|
593
|
+
RecoveryAction.SEND_STRONGER_PROMPT,
|
|
594
|
+
`Still idle after continue signal. Sending stronger prompt...`,
|
|
595
|
+
true,
|
|
596
|
+
{ timeSincePhaseChange, continueSignalCount: state.continueSignalCount }
|
|
597
|
+
);
|
|
513
598
|
}
|
|
514
599
|
break;
|
|
515
600
|
|
|
516
601
|
case StallPhase.STRONGER_PROMPT_SENT:
|
|
517
|
-
// Stronger prompt 후 유예 시간 초과
|
|
518
602
|
if (timeSincePhaseChange > this.config.strongerPromptGraceMs) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
message: `No response after stronger prompt. Killing and restarting process...`,
|
|
524
|
-
isTransient: true,
|
|
525
|
-
details: { restartCount: state.restartCount, maxRestarts: this.config.maxRestarts },
|
|
526
|
-
};
|
|
527
|
-
} else {
|
|
528
|
-
return {
|
|
529
|
-
type: StallType.IDLE,
|
|
530
|
-
action: RecoveryAction.RUN_DOCTOR,
|
|
531
|
-
message: `Lane failed after ${state.restartCount} restarts. Running diagnostics...`,
|
|
532
|
-
isTransient: false,
|
|
533
|
-
details: { restartCount: state.restartCount },
|
|
534
|
-
};
|
|
535
|
-
}
|
|
603
|
+
return this.buildRestartOrDoctorAnalysis(state,
|
|
604
|
+
'No response after stronger prompt. Killing and restarting process...',
|
|
605
|
+
`Lane failed after ${state.restartCount} restarts. Running diagnostics...`
|
|
606
|
+
);
|
|
536
607
|
}
|
|
537
608
|
break;
|
|
538
609
|
|
|
539
610
|
case StallPhase.RESTART_REQUESTED:
|
|
540
|
-
// 재시작 후 idle timeout의 75%로 더 짧게 감지
|
|
541
611
|
const postRestartTimeout = effectiveIdleTimeout * 0.75;
|
|
542
612
|
if (idleTime > postRestartTimeout) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
isTransient: true,
|
|
549
|
-
details: { idleTimeMs: idleTime, restartCount: state.restartCount },
|
|
550
|
-
};
|
|
551
|
-
} else {
|
|
552
|
-
return {
|
|
553
|
-
type: StallType.IDLE,
|
|
554
|
-
action: RecoveryAction.RUN_DOCTOR,
|
|
555
|
-
message: `Lane repeatedly stalled. Running diagnostics...`,
|
|
556
|
-
isTransient: false,
|
|
557
|
-
details: { restartCount: state.restartCount },
|
|
558
|
-
};
|
|
559
|
-
}
|
|
613
|
+
return this.buildRestartOrDoctorAnalysis(state,
|
|
614
|
+
'Lane idle after restart. Retrying continue signal...',
|
|
615
|
+
'Lane repeatedly stalled. Running diagnostics...',
|
|
616
|
+
RecoveryAction.SEND_CONTINUE // restart 후에는 continue부터 시작
|
|
617
|
+
);
|
|
560
618
|
}
|
|
561
619
|
break;
|
|
562
620
|
|
|
563
621
|
case StallPhase.DIAGNOSED:
|
|
564
622
|
case StallPhase.ABORTED:
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
type: StallType.IDLE,
|
|
568
|
-
action: RecoveryAction.ABORT_LANE,
|
|
569
|
-
message: 'Lane recovery exhausted',
|
|
570
|
-
isTransient: false,
|
|
571
|
-
};
|
|
623
|
+
return this.buildAnalysis(StallType.IDLE, RecoveryAction.ABORT_LANE, 'Lane recovery exhausted', false);
|
|
572
624
|
}
|
|
573
625
|
|
|
574
|
-
|
|
575
|
-
return {
|
|
576
|
-
type: StallType.IDLE,
|
|
577
|
-
action: RecoveryAction.NONE,
|
|
578
|
-
message: 'Monitoring',
|
|
579
|
-
isTransient: true,
|
|
580
|
-
};
|
|
626
|
+
return null;
|
|
581
627
|
}
|
|
582
628
|
|
|
583
|
-
/**
|
|
584
|
-
|
|
585
|
-
|
|
629
|
+
/** 재시작 또는 Doctor 실행 결정 헬퍼 */
|
|
630
|
+
private buildRestartOrDoctorAnalysis(
|
|
631
|
+
state: LaneStallState,
|
|
632
|
+
restartMsg: string,
|
|
633
|
+
doctorMsg: string,
|
|
634
|
+
restartAction: RecoveryAction = RecoveryAction.REQUEST_RESTART
|
|
635
|
+
): StallAnalysis {
|
|
636
|
+
const canRestart = state.restartCount < this.config.maxRestarts;
|
|
637
|
+
return this.buildAnalysis(
|
|
638
|
+
StallType.IDLE,
|
|
639
|
+
canRestart ? restartAction : RecoveryAction.RUN_DOCTOR,
|
|
640
|
+
canRestart ? restartMsg : doctorMsg,
|
|
641
|
+
canRestart,
|
|
642
|
+
{ restartCount: state.restartCount, maxRestarts: this.config.maxRestarts }
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Progress timeout에 대한 에스컬레이션 액션 결정 */
|
|
586
647
|
private getEscalatedAction(state: LaneStallState, type: StallType, progressTime: number): StallAnalysis {
|
|
648
|
+
const details = { progressTimeMs: progressTime };
|
|
649
|
+
|
|
587
650
|
switch (state.phase) {
|
|
588
651
|
case StallPhase.NORMAL:
|
|
589
|
-
return
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
message: `No progress for ${Math.round(progressTime / 60000)} minutes. Sending continue signal...`,
|
|
593
|
-
isTransient: true,
|
|
594
|
-
details: { progressTimeMs: progressTime },
|
|
595
|
-
};
|
|
652
|
+
return this.buildAnalysis(type, RecoveryAction.SEND_CONTINUE,
|
|
653
|
+
`No progress for ${Math.round(progressTime / 60000)} minutes. Sending continue signal...`,
|
|
654
|
+
true, details);
|
|
596
655
|
|
|
597
656
|
case StallPhase.CONTINUE_SENT:
|
|
598
|
-
return
|
|
599
|
-
|
|
600
|
-
action: RecoveryAction.SEND_STRONGER_PROMPT,
|
|
601
|
-
message: `Still no progress. Sending stronger prompt...`,
|
|
602
|
-
isTransient: true,
|
|
603
|
-
details: { progressTimeMs: progressTime },
|
|
604
|
-
};
|
|
657
|
+
return this.buildAnalysis(type, RecoveryAction.SEND_STRONGER_PROMPT,
|
|
658
|
+
'Still no progress. Sending stronger prompt...', true, details);
|
|
605
659
|
|
|
606
660
|
default:
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
};
|
|
615
|
-
} else {
|
|
616
|
-
return {
|
|
617
|
-
type,
|
|
618
|
-
action: RecoveryAction.RUN_DOCTOR,
|
|
619
|
-
message: `Persistent no-progress state. Running diagnostics...`,
|
|
620
|
-
isTransient: false,
|
|
621
|
-
details: { progressTimeMs: progressTime, restartCount: state.restartCount },
|
|
622
|
-
};
|
|
623
|
-
}
|
|
661
|
+
const canRestart = state.restartCount < this.config.maxRestarts;
|
|
662
|
+
return this.buildAnalysis(type,
|
|
663
|
+
canRestart ? RecoveryAction.REQUEST_RESTART : RecoveryAction.RUN_DOCTOR,
|
|
664
|
+
canRestart ? 'No progress after interventions. Restarting...' : 'Persistent no-progress state. Running diagnostics...',
|
|
665
|
+
canRestart,
|
|
666
|
+
{ ...details, restartCount: state.restartCount }
|
|
667
|
+
);
|
|
624
668
|
}
|
|
625
669
|
}
|
|
626
670
|
|
|
@@ -140,11 +140,23 @@ export function withContext(context: string) {
|
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* Lane-specific output
|
|
143
|
+
* @param laneName - Lane name
|
|
144
|
+
* @param message - Message to output
|
|
145
|
+
* @param isError - Whether this is an error message
|
|
146
|
+
* @param laneIndex - Optional lane index
|
|
147
|
+
* @param taskIndex - Optional task index
|
|
148
|
+
* @param taskName - Optional task name
|
|
143
149
|
*/
|
|
144
|
-
export function laneOutput(laneName: string, message: string, isError = false): void {
|
|
150
|
+
export function laneOutput(laneName: string, message: string, isError = false, laneIndex?: number, taskIndex?: number, taskName?: string): void {
|
|
145
151
|
const timestamp = `${COLORS.gray}[${formatTimestamp()}]${COLORS.reset}`;
|
|
146
|
-
|
|
147
|
-
const
|
|
152
|
+
// Format: [laneIdx-taskIdx-laneName-taskName] padded to 18 chars inside brackets
|
|
153
|
+
const lIdx = laneIndex ?? 1;
|
|
154
|
+
const tIdx = taskIndex ?? 1;
|
|
155
|
+
const combined = taskName
|
|
156
|
+
? `${lIdx}-${tIdx}-${laneName}-${taskName}`
|
|
157
|
+
: `${lIdx}-${tIdx}-${laneName}`;
|
|
158
|
+
const label = combined.substring(0, 18).padEnd(18);
|
|
159
|
+
const laneLabel = `${COLORS.magenta}[${label}]${COLORS.reset}`;
|
|
148
160
|
const output = isError ? `${COLORS.red}${message}${COLORS.reset}` : message;
|
|
149
161
|
|
|
150
162
|
if (isError) {
|
|
@@ -231,6 +231,8 @@ function getTypeInfo(type: MessageType): { label: string; color: string } {
|
|
|
231
231
|
info: { label: 'INFO ', color: COLORS.cyan },
|
|
232
232
|
warn: { label: 'WARN ', color: COLORS.yellow },
|
|
233
233
|
error: { label: 'ERROR ', color: COLORS.red },
|
|
234
|
+
debug: { label: 'DEBUG ', color: COLORS.gray },
|
|
235
|
+
progress: { label: 'PROG ', color: COLORS.blue || COLORS.cyan },
|
|
234
236
|
stdout: { label: 'STDOUT', color: COLORS.white },
|
|
235
237
|
stderr: { label: 'STDERR', color: COLORS.red },
|
|
236
238
|
};
|
package/src/types/config.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface CursorFlowConfig {
|
|
|
69
69
|
maxConcurrentLanes: number;
|
|
70
70
|
projectRoot: string;
|
|
71
71
|
/** Output format for cursor-agent (default: 'json') */
|
|
72
|
-
agentOutputFormat: 'json' | 'plain';
|
|
72
|
+
agentOutputFormat: 'json' | 'plain' | 'stream-json';
|
|
73
73
|
webhooks?: WebhookConfig[];
|
|
74
74
|
/** Enhanced logging configuration */
|
|
75
75
|
enhancedLogging?: Partial<EnhancedLogConfig>;
|
package/src/types/logging.ts
CHANGED
package/src/types/task.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Task {
|
|
|
17
17
|
|
|
18
18
|
export interface RunnerConfig {
|
|
19
19
|
tasks: Task[];
|
|
20
|
+
dependsOn?: string[];
|
|
20
21
|
pipelineBranch?: string;
|
|
21
22
|
worktreeDir?: string;
|
|
22
23
|
branchPrefix?: string;
|
|
@@ -25,7 +26,7 @@ export interface RunnerConfig {
|
|
|
25
26
|
model?: string;
|
|
26
27
|
dependencyPolicy: DependencyPolicy;
|
|
27
28
|
/** Output format for cursor-agent (default: 'json') */
|
|
28
|
-
agentOutputFormat?: 'json' | 'plain';
|
|
29
|
+
agentOutputFormat?: 'json' | 'plain' | 'stream-json';
|
|
29
30
|
/** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
|
|
30
31
|
timeout?: number;
|
|
31
32
|
/**
|
|
@@ -34,12 +35,6 @@ export interface RunnerConfig {
|
|
|
34
35
|
* Default: false
|
|
35
36
|
*/
|
|
36
37
|
enableIntervention?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Disable Git operations (worktree, branch, push, commit).
|
|
39
|
-
* Useful for testing or environments without Git remote.
|
|
40
|
-
* Default: false
|
|
41
|
-
*/
|
|
42
|
-
noGit?: boolean;
|
|
43
38
|
/**
|
|
44
39
|
* Enable verbose Git logging.
|
|
45
40
|
* Default: false
|
package/src/utils/doctor.ts
CHANGED
|
@@ -618,7 +618,7 @@ function validateBranchNames(
|
|
|
618
618
|
/**
|
|
619
619
|
* Status file to track when doctor was last run successfully.
|
|
620
620
|
*/
|
|
621
|
-
const DOCTOR_STATUS_FILE = '
|
|
621
|
+
const DOCTOR_STATUS_FILE = '_cursorflow/doctor-status.json';
|
|
622
622
|
|
|
623
623
|
export function saveDoctorStatus(repoRoot: string, report: DoctorReport): void {
|
|
624
624
|
const statusPath = safeJoin(repoRoot, DOCTOR_STATUS_FILE);
|
|
@@ -720,6 +720,10 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
|
|
|
720
720
|
}
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
+
const repoRoot = resolveRepoRoot(cwd) || undefined;
|
|
724
|
+
context.repoRoot = repoRoot;
|
|
725
|
+
const gitCwd = repoRoot || cwd;
|
|
726
|
+
|
|
723
727
|
// 1) Git repository checks
|
|
724
728
|
if (!isInsideGitWorktree(cwd)) {
|
|
725
729
|
addIssue(issues, {
|
|
@@ -736,10 +740,6 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
|
|
|
736
740
|
return { ok: false, issues, context };
|
|
737
741
|
}
|
|
738
742
|
|
|
739
|
-
const repoRoot = resolveRepoRoot(cwd) || undefined;
|
|
740
|
-
context.repoRoot = repoRoot;
|
|
741
|
-
const gitCwd = repoRoot || cwd;
|
|
742
|
-
|
|
743
743
|
if (!hasAtLeastOneCommit(gitCwd)) {
|
|
744
744
|
addIssue(issues, {
|
|
745
745
|
id: 'git.no_commits',
|
package/src/utils/git.ts
CHANGED
|
@@ -156,6 +156,7 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
156
156
|
cwd: cwd || process.cwd(),
|
|
157
157
|
encoding: 'utf8',
|
|
158
158
|
stdio: stdioMode as any,
|
|
159
|
+
timeout: 30000, // 30 second timeout for all git operations
|
|
159
160
|
});
|
|
160
161
|
|
|
161
162
|
if (result.error) {
|
|
@@ -206,6 +207,7 @@ export function runGitResult(args: string[], options: GitRunOptions = {}): GitRe
|
|
|
206
207
|
cwd: cwd || process.cwd(),
|
|
207
208
|
encoding: 'utf8',
|
|
208
209
|
stdio: 'pipe',
|
|
210
|
+
timeout: 30000, // 30 second timeout
|
|
209
211
|
});
|
|
210
212
|
|
|
211
213
|
const gitResult = {
|
package/src/utils/health.ts
CHANGED
|
@@ -455,14 +455,6 @@ export async function preflightCheck(options: {
|
|
|
455
455
|
blockers.push(`Git: ${gitHealth.message}`);
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
// Check authentication
|
|
459
|
-
if (options.requireAuth !== false) {
|
|
460
|
-
const authHealth = await checkAuthHealth();
|
|
461
|
-
if (!authHealth.ok) {
|
|
462
|
-
blockers.push(`Authentication: ${authHealth.message}`);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
458
|
// Check Git remote (warning only unless required)
|
|
467
459
|
const remoteHealth = await checkGitRemoteHealth(options.cwd);
|
|
468
460
|
if (!remoteHealth.ok) {
|
|
@@ -473,6 +465,21 @@ export async function preflightCheck(options: {
|
|
|
473
465
|
}
|
|
474
466
|
}
|
|
475
467
|
|
|
468
|
+
// Check worktrees
|
|
469
|
+
const worktreeHealth = await checkWorktrees(options.cwd);
|
|
470
|
+
if (!worktreeHealth.ok) {
|
|
471
|
+
warnings.push(`Worktrees: ${worktreeHealth.message}`);
|
|
472
|
+
recommendations.push('Run `cursorflow clean worktrees` to clean up orphaned worktrees');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Check authentication
|
|
476
|
+
if (options.requireAuth !== false) {
|
|
477
|
+
const authHealth = await checkAuthHealth();
|
|
478
|
+
if (!authHealth.ok) {
|
|
479
|
+
blockers.push(`Authentication: ${authHealth.message}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
476
483
|
// Check disk space
|
|
477
484
|
const diskHealth = await checkDiskSpace();
|
|
478
485
|
if (!diskHealth.ok) {
|
|
@@ -486,13 +493,6 @@ export async function preflightCheck(options: {
|
|
|
486
493
|
recommendations.push('Run `cursorflow clean locks` to remove stale locks');
|
|
487
494
|
}
|
|
488
495
|
|
|
489
|
-
// Check worktrees
|
|
490
|
-
const worktreeHealth = await checkWorktrees(options.cwd);
|
|
491
|
-
if (!worktreeHealth.ok) {
|
|
492
|
-
warnings.push(`Worktrees: ${worktreeHealth.message}`);
|
|
493
|
-
recommendations.push('Run `cursorflow clean worktrees` to clean up orphaned worktrees');
|
|
494
|
-
}
|
|
495
|
-
|
|
496
496
|
// Check system resources
|
|
497
497
|
const resourceHealth = await checkSystemResources();
|
|
498
498
|
if (!resourceHealth.ok) {
|
|
@@ -44,12 +44,17 @@ export function formatMessageForConsole(
|
|
|
44
44
|
const ts = includeTimestamp ? new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
45
45
|
const tsPrefix = ts ? `${COLORS.gray}[${ts}]${COLORS.reset} ` : '';
|
|
46
46
|
|
|
47
|
-
// Handle context (e.g. from logger.info) - max
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// Handle context (e.g. from logger.info) - max 18 chars inside brackets
|
|
48
|
+
// Format: [1-1-lanename1234] padded to fixed width 20 (including brackets)
|
|
49
|
+
let effectiveLaneLabel = laneLabel || (context ? `[${context.substring(0, 18).padEnd(18)}]` : '');
|
|
50
|
+
|
|
51
|
+
// Smart truncation: ensure it always ends with ]
|
|
52
|
+
if (effectiveLaneLabel.length > 20) {
|
|
53
|
+
effectiveLaneLabel = effectiveLaneLabel.substring(0, 19) + ']';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fixed width 20 chars for consistent alignment
|
|
57
|
+
const labelPrefix = effectiveLaneLabel ? `${COLORS.magenta}${effectiveLaneLabel.padEnd(20)}${COLORS.reset} ` : '';
|
|
53
58
|
|
|
54
59
|
let typePrefix = '';
|
|
55
60
|
let content = msg.content;
|
|
@@ -155,6 +160,44 @@ export function formatMessageForConsole(
|
|
|
155
160
|
|
|
156
161
|
if (!typePrefix) return `${tsPrefix}${labelPrefix}${content}`;
|
|
157
162
|
|
|
163
|
+
// Avoid double prefixes (e.g. INFO INFO)
|
|
164
|
+
const plainTypePrefix = stripAnsi(typePrefix).replace(/[^\x00-\x7F]/g, '').trim(); // "INFO", "DONE", etc.
|
|
165
|
+
const plainContent = stripAnsi(content);
|
|
166
|
+
if (plainContent.includes(` ${plainTypePrefix} `) || plainContent.startsWith(`${plainTypePrefix} `)) {
|
|
167
|
+
// If content already has the prefix, try to strip it from content or just use content as is
|
|
168
|
+
// For simplicity, if it's already there, we can just skip adding our typePrefix
|
|
169
|
+
// but we still want the colors. This is tricky.
|
|
170
|
+
// Usually it's better to just return the content if it looks already formatted.
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A better way: if content starts with an emoji that matches our type, skip typePrefix
|
|
174
|
+
const emojiMap: Record<string, string> = {
|
|
175
|
+
'info': 'ℹ️',
|
|
176
|
+
'success': '✅',
|
|
177
|
+
'result': '✅',
|
|
178
|
+
'warn': '⚠️',
|
|
179
|
+
'error': '❌',
|
|
180
|
+
'tool': '🔧',
|
|
181
|
+
'thinking': '🤔',
|
|
182
|
+
'user': '🧑',
|
|
183
|
+
'assistant': '🤖'
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const targetEmoji = emojiMap[msg.type];
|
|
187
|
+
if (targetEmoji && plainContent.trim().startsWith(targetEmoji)) {
|
|
188
|
+
return `${tsPrefix}${labelPrefix}${content}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle separator lines - if content is just a repeat of ━ or ─, extend it
|
|
192
|
+
const separatorMatch = content.match(/^([━─=]+)$/);
|
|
193
|
+
if (separatorMatch) {
|
|
194
|
+
const char = separatorMatch[1]![0]!;
|
|
195
|
+
// Use a fixed width for now (80 is a good standard)
|
|
196
|
+
// In a real terminal we could use process.stdout.columns
|
|
197
|
+
const targetWidth = 80;
|
|
198
|
+
content = char.repeat(targetWidth);
|
|
199
|
+
}
|
|
200
|
+
|
|
158
201
|
// Compact format (single line)
|
|
159
202
|
if (!useBox) {
|
|
160
203
|
return `${tsPrefix}${labelPrefix}${typePrefix.padEnd(12)} ${content}`;
|
|
@@ -169,7 +212,7 @@ export function formatMessageForConsole(
|
|
|
169
212
|
const emojiCount = (strippedPrefix.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2300}-\u{23FF}]|[\u{2B50}-\u{2B55}]|[\u{231A}-\u{231B}]|[\u{23E9}-\u{23F3}]|[\u{23F8}-\u{23FA}]|✅|❌|⚙️|ℹ️|⚠️|🔧|📄|🤔|🧑|🤖/gu) || []).length;
|
|
170
213
|
const visualWidth = strippedPrefix.length + emojiCount; // emoji adds 1 extra width
|
|
171
214
|
|
|
172
|
-
const boxWidth =
|
|
215
|
+
const boxWidth = 80;
|
|
173
216
|
const header = `${typePrefix}┌${'─'.repeat(boxWidth)}`;
|
|
174
217
|
let result = `${fullPrefix}${header}\n`;
|
|
175
218
|
|