@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.
Files changed (84) hide show
  1. package/commands/cursorflow-init.md +113 -32
  2. package/commands/cursorflow-prepare.md +146 -339
  3. package/commands/cursorflow-run.md +148 -131
  4. package/dist/cli/add.js +8 -4
  5. package/dist/cli/add.js.map +1 -1
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/new.js +3 -5
  9. package/dist/cli/new.js.map +1 -1
  10. package/dist/cli/resume.js +26 -15
  11. package/dist/cli/resume.js.map +1 -1
  12. package/dist/cli/run.js +1 -6
  13. package/dist/cli/run.js.map +1 -1
  14. package/dist/cli/setup-commands.d.ts +1 -0
  15. package/dist/cli/setup-commands.js +1 -0
  16. package/dist/cli/setup-commands.js.map +1 -1
  17. package/dist/core/runner/agent.d.ts +5 -1
  18. package/dist/core/runner/agent.js +31 -1
  19. package/dist/core/runner/agent.js.map +1 -1
  20. package/dist/core/runner/pipeline.d.ts +0 -1
  21. package/dist/core/runner/pipeline.js +116 -167
  22. package/dist/core/runner/pipeline.js.map +1 -1
  23. package/dist/core/runner/prompt.d.ts +0 -1
  24. package/dist/core/runner/prompt.js +11 -16
  25. package/dist/core/runner/prompt.js.map +1 -1
  26. package/dist/core/runner/task.d.ts +1 -2
  27. package/dist/core/runner/task.js +24 -36
  28. package/dist/core/runner/task.js.map +1 -1
  29. package/dist/core/runner.js +12 -2
  30. package/dist/core/runner.js.map +1 -1
  31. package/dist/core/stall-detection.d.ts +16 -4
  32. package/dist/core/stall-detection.js +97 -148
  33. package/dist/core/stall-detection.js.map +1 -1
  34. package/dist/services/logging/console.d.ts +7 -1
  35. package/dist/services/logging/console.js +15 -3
  36. package/dist/services/logging/console.js.map +1 -1
  37. package/dist/services/logging/formatter.js +2 -0
  38. package/dist/services/logging/formatter.js.map +1 -1
  39. package/dist/types/config.d.ts +1 -1
  40. package/dist/types/logging.d.ts +1 -1
  41. package/dist/types/task.d.ts +2 -7
  42. package/dist/utils/doctor.js +4 -4
  43. package/dist/utils/doctor.js.map +1 -1
  44. package/dist/utils/git.js +2 -0
  45. package/dist/utils/git.js.map +1 -1
  46. package/dist/utils/health.js +13 -13
  47. package/dist/utils/health.js.map +1 -1
  48. package/dist/utils/log-formatter.js +44 -7
  49. package/dist/utils/log-formatter.js.map +1 -1
  50. package/dist/utils/logger.js +2 -2
  51. package/dist/utils/logger.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/cli/add.ts +9 -4
  54. package/src/cli/index.ts +3 -0
  55. package/src/cli/new.ts +3 -5
  56. package/src/cli/resume.ts +30 -19
  57. package/src/cli/run.ts +1 -6
  58. package/src/cli/setup-commands.ts +1 -1
  59. package/src/core/runner/agent.ts +36 -4
  60. package/src/core/runner/pipeline.ts +127 -176
  61. package/src/core/runner/prompt.ts +11 -18
  62. package/src/core/runner/task.ts +24 -37
  63. package/src/core/runner.ts +13 -2
  64. package/src/core/stall-detection.ts +190 -146
  65. package/src/services/logging/console.ts +15 -3
  66. package/src/services/logging/formatter.ts +2 -0
  67. package/src/types/config.ts +1 -1
  68. package/src/types/logging.ts +4 -2
  69. package/src/types/task.ts +2 -7
  70. package/src/utils/doctor.ts +5 -5
  71. package/src/utils/git.ts +2 -0
  72. package/src/utils/health.ts +15 -15
  73. package/src/utils/log-formatter.ts +50 -7
  74. package/src/utils/logger.ts +2 -2
  75. package/commands/cursorflow-add.md +0 -159
  76. package/commands/cursorflow-clean.md +0 -84
  77. package/commands/cursorflow-doctor.md +0 -102
  78. package/commands/cursorflow-models.md +0 -51
  79. package/commands/cursorflow-monitor.md +0 -90
  80. package/commands/cursorflow-new.md +0 -87
  81. package/commands/cursorflow-resume.md +0 -205
  82. package/commands/cursorflow-signal.md +0 -52
  83. package/commands/cursorflow-stop.md +0 -55
  84. 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 → AGENT_NO_RESPONSE
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 now = Date.now();
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 bytesDelta = state.totalBytesReceived - state.bytesAtLastCheck;
466
+ // 1. Task timeout (최우선)
467
+ const taskResult = this.checkTaskTimeout(ctx);
468
+ if (taskResult) return taskResult;
451
469
 
452
- // 장기 작업 유예 시간 적용
453
- const effectiveIdleTimeout = state.isLongOperation
454
- ? this.config.longOperationGraceMs
455
- : this.config.idleTimeoutMs;
470
+ // 2. Zero bytes + idle (에이전트 무응답)
471
+ const zeroByteResult = this.checkZeroBytes(ctx);
472
+ if (zeroByteResult) return zeroByteResult;
456
473
 
457
- // 1. Task timeout 체크 (최우선)
458
- if (taskTime > this.config.taskTimeoutMs) {
459
- return {
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
- // 2. Zero bytes + idle 체크 (에이전트 무응답)
471
- if (bytesDelta === 0 && idleTime > effectiveIdleTimeout) {
472
- return {
473
- type: StallType.ZERO_BYTES,
474
- action: state.phase < StallPhase.STRONGER_PROMPT_SENT
475
- ? RecoveryAction.SEND_CONTINUE
476
- : RecoveryAction.REQUEST_RESTART,
477
- message: `Agent produced 0 bytes for ${Math.round(idleTime / 1000)}s - possible API issue`,
478
- isTransient: true,
479
- details: { idleTimeMs: idleTime, bytesDelta, phase: state.phase },
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
- // 3. Progress timeout 체크
484
- if (progressTime > this.config.progressTimeoutMs) {
485
- return this.getEscalatedAction(state, StallType.NO_PROGRESS, progressTime);
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
- type: StallType.IDLE,
495
- action: RecoveryAction.SEND_CONTINUE,
496
- message: `Lane idle for ${Math.round(idleTime / 1000)}s. Sending continue signal...`,
497
- isTransient: true,
498
- details: { idleTimeMs: idleTime, isLongOperation: state.isLongOperation },
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
- type: StallType.IDLE,
508
- action: RecoveryAction.SEND_STRONGER_PROMPT,
509
- message: `Still idle after continue signal. Sending stronger prompt...`,
510
- isTransient: true,
511
- details: { timeSincePhaseChange, continueSignalCount: state.continueSignalCount },
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
- if (state.restartCount < this.config.maxRestarts) {
520
- return {
521
- type: StallType.IDLE,
522
- action: RecoveryAction.REQUEST_RESTART,
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
- if (state.restartCount < this.config.maxRestarts) {
544
- return {
545
- type: StallType.IDLE,
546
- action: RecoveryAction.SEND_CONTINUE,
547
- message: `Lane idle after restart. Retrying continue signal...`,
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
- * Progress timeout에 대한 에스컬레이션 액션 결정
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
- type,
591
- action: RecoveryAction.SEND_CONTINUE,
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
- type,
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
- if (state.restartCount < this.config.maxRestarts) {
608
- return {
609
- type,
610
- action: RecoveryAction.REQUEST_RESTART,
611
- message: `No progress after interventions. Restarting...`,
612
- isTransient: true,
613
- details: { progressTimeMs: progressTime, restartCount: state.restartCount },
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
- const shortName = laneName.substring(0, 10).padEnd(10);
147
- const laneLabel = `${COLORS.magenta}${shortName}${COLORS.reset}`;
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
  };
@@ -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>;
@@ -22,8 +22,10 @@ export type MessageType =
22
22
  | 'success'
23
23
  | 'info'
24
24
  | 'warn'
25
- | 'error'
26
- | 'stdout'
25
+ | 'error'
26
+ | 'debug'
27
+ | 'progress'
28
+ | 'stdout'
27
29
  | 'stderr';
28
30
 
29
31
  export interface ParsedMessage {
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
@@ -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 = '.cursorflow/doctor-status.json';
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 = {
@@ -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 16 chars
48
- const effectiveLaneLabel = laneLabel || (context ? `[${context}]` : '');
49
- const truncatedLabel = effectiveLaneLabel.length > 16
50
- ? effectiveLaneLabel.substring(0, 16)
51
- : effectiveLaneLabel;
52
- const labelPrefix = truncatedLabel ? `${COLORS.magenta}${truncatedLabel.padEnd(16)}${COLORS.reset} ` : '';
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 = 60;
215
+ const boxWidth = 80;
173
216
  const header = `${typePrefix}┌${'─'.repeat(boxWidth)}`;
174
217
  let result = `${fullPrefix}${header}\n`;
175
218