@litmers/cursorflow-orchestrator 0.1.37 → 0.1.40

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 (102) hide show
  1. package/README.md +13 -13
  2. package/commands/cursorflow-init.md +113 -32
  3. package/commands/cursorflow-prepare.md +146 -339
  4. package/commands/cursorflow-run.md +148 -131
  5. package/dist/cli/add.js +8 -4
  6. package/dist/cli/add.js.map +1 -1
  7. package/dist/cli/index.js +2 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/new.js +3 -5
  10. package/dist/cli/new.js.map +1 -1
  11. package/dist/cli/prepare.js +0 -1
  12. package/dist/cli/prepare.js.map +1 -1
  13. package/dist/cli/resume.js +24 -15
  14. package/dist/cli/resume.js.map +1 -1
  15. package/dist/cli/run.js +1 -6
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/cli/setup-commands.d.ts +1 -0
  18. package/dist/cli/setup-commands.js +1 -0
  19. package/dist/cli/setup-commands.js.map +1 -1
  20. package/dist/core/orchestrator.js +13 -5
  21. package/dist/core/orchestrator.js.map +1 -1
  22. package/dist/core/runner/agent.d.ts +5 -1
  23. package/dist/core/runner/agent.js +31 -1
  24. package/dist/core/runner/agent.js.map +1 -1
  25. package/dist/core/runner/pipeline.d.ts +0 -1
  26. package/dist/core/runner/pipeline.js +136 -173
  27. package/dist/core/runner/pipeline.js.map +1 -1
  28. package/dist/core/runner/prompt.d.ts +0 -1
  29. package/dist/core/runner/prompt.js +11 -16
  30. package/dist/core/runner/prompt.js.map +1 -1
  31. package/dist/core/runner/task.d.ts +1 -2
  32. package/dist/core/runner/task.js +31 -40
  33. package/dist/core/runner/task.js.map +1 -1
  34. package/dist/core/runner.js +15 -2
  35. package/dist/core/runner.js.map +1 -1
  36. package/dist/core/stall-detection.d.ts +32 -4
  37. package/dist/core/stall-detection.js +151 -149
  38. package/dist/core/stall-detection.js.map +1 -1
  39. package/dist/services/logging/console.d.ts +7 -1
  40. package/dist/services/logging/console.js +13 -3
  41. package/dist/services/logging/console.js.map +1 -1
  42. package/dist/services/logging/formatter.d.ts +1 -0
  43. package/dist/services/logging/formatter.js +6 -3
  44. package/dist/services/logging/formatter.js.map +1 -1
  45. package/dist/types/config.d.ts +3 -1
  46. package/dist/types/logging.d.ts +1 -1
  47. package/dist/types/task.d.ts +3 -8
  48. package/dist/utils/config.js +5 -0
  49. package/dist/utils/config.js.map +1 -1
  50. package/dist/utils/doctor.js +4 -4
  51. package/dist/utils/doctor.js.map +1 -1
  52. package/dist/utils/enhanced-logger.d.ts +1 -1
  53. package/dist/utils/enhanced-logger.js +3 -3
  54. package/dist/utils/enhanced-logger.js.map +1 -1
  55. package/dist/utils/git.d.ts +12 -1
  56. package/dist/utils/git.js +56 -1
  57. package/dist/utils/git.js.map +1 -1
  58. package/dist/utils/health.js +13 -13
  59. package/dist/utils/health.js.map +1 -1
  60. package/dist/utils/log-formatter.d.ts +1 -1
  61. package/dist/utils/log-formatter.js +45 -8
  62. package/dist/utils/log-formatter.js.map +1 -1
  63. package/dist/utils/logger.js +2 -2
  64. package/dist/utils/logger.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/cli/add.ts +9 -4
  67. package/src/cli/index.ts +3 -0
  68. package/src/cli/new.ts +3 -5
  69. package/src/cli/prepare.ts +0 -1
  70. package/src/cli/resume.ts +28 -19
  71. package/src/cli/run.ts +1 -6
  72. package/src/cli/setup-commands.ts +1 -1
  73. package/src/core/orchestrator.ts +14 -5
  74. package/src/core/runner/agent.ts +36 -4
  75. package/src/core/runner/pipeline.ts +149 -182
  76. package/src/core/runner/prompt.ts +11 -18
  77. package/src/core/runner/task.ts +32 -41
  78. package/src/core/runner.ts +17 -2
  79. package/src/core/stall-detection.ts +263 -147
  80. package/src/services/logging/console.ts +13 -3
  81. package/src/services/logging/formatter.ts +6 -3
  82. package/src/types/config.ts +3 -1
  83. package/src/types/logging.ts +4 -2
  84. package/src/types/task.ts +3 -8
  85. package/src/utils/config.ts +6 -0
  86. package/src/utils/doctor.ts +5 -5
  87. package/src/utils/enhanced-logger.ts +3 -3
  88. package/src/utils/flow.ts +1 -0
  89. package/src/utils/git.ts +61 -1
  90. package/src/utils/health.ts +15 -15
  91. package/src/utils/log-formatter.ts +51 -8
  92. package/src/utils/logger.ts +2 -2
  93. package/commands/cursorflow-add.md +0 -159
  94. package/commands/cursorflow-clean.md +0 -84
  95. package/commands/cursorflow-doctor.md +0 -102
  96. package/commands/cursorflow-models.md +0 -51
  97. package/commands/cursorflow-monitor.md +0 -90
  98. package/commands/cursorflow-new.md +0 -87
  99. package/commands/cursorflow-resume.md +0 -205
  100. package/commands/cursorflow-signal.md +0 -52
  101. package/commands/cursorflow-stop.md +0 -55
  102. package/commands/cursorflow-triggers.md +0 -250
@@ -18,6 +18,7 @@ export * from './runner/index';
18
18
 
19
19
  // Import necessary parts for the CLI entry point
20
20
  import { runTasks } from './runner/pipeline';
21
+ import { cleanupAgentChildren } from './runner/agent';
21
22
 
22
23
  /**
23
24
  * CLI entry point
@@ -35,7 +36,6 @@ if (require.main === module) {
35
36
  const startIdxIdx = args.indexOf('--start-index');
36
37
  const pipelineBranchIdx = args.indexOf('--pipeline-branch');
37
38
  const worktreeDirIdx = args.indexOf('--worktree-dir');
38
- const noGit = args.includes('--no-git');
39
39
 
40
40
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
41
41
  const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
@@ -89,8 +89,23 @@ if (require.main === module) {
89
89
  // Add agent output format default
90
90
  config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'json';
91
91
 
92
+ // Merge intervention and logging settings
93
+ config.enableIntervention = config.enableIntervention ?? globalConfig?.enableIntervention ?? true;
94
+ config.verboseGit = config.verboseGit ?? globalConfig?.verboseGit ?? false;
95
+
96
+ // Handle process interruption to ensure cleanup
97
+ const handleSignal = (signal: string) => {
98
+ logger.warn(`\n⚠️ Runner received ${signal}. Shutting down...`);
99
+ // Cleanup any active agent child processes
100
+ cleanupAgentChildren();
101
+ process.exit(1);
102
+ };
103
+
104
+ process.on('SIGINT', () => handleSignal('SIGINT'));
105
+ process.on('SIGTERM', () => handleSignal('SIGTERM'));
106
+
92
107
  // Run tasks
93
- runTasks(tasksFile, config, runDir, { startIndex, noGit })
108
+ runTasks(tasksFile, config, runDir, { startIndex })
94
109
  .then(() => {
95
110
  process.exit(0);
96
111
  })
@@ -142,6 +142,10 @@ export interface LaneStallState {
142
142
  laneName: string;
143
143
  /** 현재 복구 단계 */
144
144
  phase: StallPhase;
145
+ /** Lane의 현재 상태 (waiting, running 등) - waiting 시 stall 분석 스킵 */
146
+ laneStatus?: string;
147
+ /** Intervention 활성화 여부 - false면 continue 신호 스킵 */
148
+ interventionEnabled?: boolean;
145
149
  /** 마지막 실제 활동 시간 (bytes > 0) */
146
150
  lastRealActivityTime: number;
147
151
  /** 마지막 상태 변경 시간 (phase 변경) */
@@ -201,6 +205,21 @@ export interface FailureRecord {
201
205
  lastOutput: string;
202
206
  }
203
207
 
208
+ // ============================================================================
209
+ // 분석 컨텍스트 (Analysis Context)
210
+ // ============================================================================
211
+
212
+ /** 분석에 필요한 시간 및 상태 컨텍스트 */
213
+ interface AnalysisContext {
214
+ state: LaneStallState;
215
+ idleTime: number;
216
+ progressTime: number;
217
+ taskTime: number;
218
+ timeSincePhaseChange: number;
219
+ bytesDelta: number;
220
+ effectiveIdleTimeout: number;
221
+ }
222
+
204
223
  // ============================================================================
205
224
  // Stall Detection Service
206
225
  // ============================================================================
@@ -279,6 +298,7 @@ export class StallDetectionService {
279
298
  laneRunDir?: string;
280
299
  childProcess?: ChildProcess;
281
300
  startIndex?: number;
301
+ interventionEnabled?: boolean;
282
302
  } = {}
283
303
  ): void {
284
304
  const now = Date.now();
@@ -286,6 +306,7 @@ export class StallDetectionService {
286
306
  this.laneStates.set(laneName, {
287
307
  laneName,
288
308
  phase: StallPhase.NORMAL,
309
+ interventionEnabled: options.interventionEnabled ?? true, // default to true
289
310
  lastRealActivityTime: now,
290
311
  lastPhaseChangeTime: now,
291
312
  lastStateUpdateTime: now,
@@ -303,7 +324,7 @@ export class StallDetectionService {
303
324
  });
304
325
 
305
326
  if (this.config.verbose) {
306
- logger.debug(`[StallService] Lane registered: ${laneName}`);
327
+ logger.debug(`[StallService] Lane registered: ${laneName} (intervention: ${options.interventionEnabled ?? true})`);
307
328
  }
308
329
  }
309
330
 
@@ -345,6 +366,44 @@ export class StallDetectionService {
345
366
  }
346
367
  }
347
368
 
369
+ /**
370
+ * Lane 상태 업데이트 (waiting, running 등)
371
+ * waiting 상태일 때는 stall 분석을 스킵함
372
+ */
373
+ setLaneStatus(laneName: string, status: string): void {
374
+ const state = this.laneStates.get(laneName);
375
+ if (state) {
376
+ state.laneStatus = status;
377
+
378
+ // waiting 상태로 전환 시 타이머 리셋 (의존성 대기 시간을 stall로 간주하지 않음)
379
+ if (status === 'waiting') {
380
+ const now = Date.now();
381
+ state.lastRealActivityTime = now;
382
+ state.lastStateUpdateTime = now;
383
+ state.taskStartTime = now;
384
+ }
385
+
386
+ if (this.config.verbose) {
387
+ logger.debug(`[StallService] [${laneName}] Lane status updated: ${status}`);
388
+ }
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Intervention 활성화 상태 설정
394
+ * false로 설정하면 continue 신호를 보내지 않음
395
+ */
396
+ setInterventionEnabled(laneName: string, enabled: boolean): void {
397
+ const state = this.laneStates.get(laneName);
398
+ if (state) {
399
+ state.interventionEnabled = enabled;
400
+
401
+ if (this.config.verbose) {
402
+ logger.debug(`[StallService] [${laneName}] Intervention ${enabled ? 'enabled' : 'disabled'}`);
403
+ }
404
+ }
405
+ }
406
+
348
407
  // --------------------------------------------------------------------------
349
408
  // 활동 기록 (Activity Recording)
350
409
  // --------------------------------------------------------------------------
@@ -420,207 +479,248 @@ export class StallDetectionService {
420
479
  // Stall 분석 (Analysis)
421
480
  // --------------------------------------------------------------------------
422
481
 
482
+ /** StallAnalysis 생성 헬퍼 */
483
+ private buildAnalysis(
484
+ type: StallType,
485
+ action: RecoveryAction,
486
+ message: string,
487
+ isTransient: boolean,
488
+ details?: Record<string, any>
489
+ ): StallAnalysis {
490
+ return { type, action, message, isTransient, details };
491
+ }
492
+
423
493
  /**
424
494
  * Stall 상태 분석 - 현재 상태에서 필요한 액션 결정
425
495
  *
426
496
  * 분석 우선순위:
497
+ * 0. waiting 상태면 스킵 (의존성 대기 중)
427
498
  * 1. Task timeout (30분) → RESTART/DOCTOR
428
- * 2. Zero bytes + idle → AGENT_NO_RESPONSE
499
+ * 2. Zero bytes + idle → phase별 에스컬레이션
429
500
  * 3. No progress (10분) → 단계별 에스컬레이션
430
501
  * 4. Idle timeout (2분) → 단계별 에스컬레이션
431
502
  */
432
503
  analyzeStall(laneName: string): StallAnalysis {
433
504
  const state = this.laneStates.get(laneName);
434
505
  if (!state) {
435
- return {
436
- type: StallType.IDLE,
437
- action: RecoveryAction.NONE,
438
- message: 'Lane not found',
439
- isTransient: false,
440
- };
506
+ return this.buildAnalysis(StallType.IDLE, RecoveryAction.NONE, 'Lane not found', false);
441
507
  }
442
508
 
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;
509
+ // 0. waiting 상태는 stall 분석 스킵 (의존성 대기 중이므로 정상)
510
+ if (state.laneStatus === 'waiting') {
511
+ return this.buildAnalysis(
512
+ StallType.IDLE,
513
+ RecoveryAction.NONE,
514
+ 'Waiting for dependencies',
515
+ true,
516
+ { laneStatus: 'waiting' }
517
+ );
518
+ }
448
519
 
449
- // 바이트 델타 계산
450
- const bytesDelta = state.totalBytesReceived - state.bytesAtLastCheck;
520
+ const ctx = this.buildAnalysisContext(state);
451
521
 
452
- // 장기 작업 유예 시간 적용
453
- const effectiveIdleTimeout = state.isLongOperation
454
- ? this.config.longOperationGraceMs
455
- : this.config.idleTimeoutMs;
522
+ // 1. Task timeout (최우선)
523
+ const taskResult = this.checkTaskTimeout(ctx);
524
+ if (taskResult) return taskResult;
456
525
 
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
- }
526
+ // 2. Zero bytes + idle (에이전트 무응답)
527
+ const zeroByteResult = this.checkZeroBytes(ctx);
528
+ if (zeroByteResult) return zeroByteResult;
469
529
 
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
- }
530
+ // 3. Progress timeout
531
+ const progressResult = this.checkProgressTimeout(ctx);
532
+ if (progressResult) return progressResult;
482
533
 
483
- // 3. Progress timeout 체크
484
- if (progressTime > this.config.progressTimeoutMs) {
485
- return this.getEscalatedAction(state, StallType.NO_PROGRESS, progressTime);
534
+ // 4. Phase별 idle 체크
535
+ const idleResult = this.checkPhaseBasedIdle(ctx);
536
+ if (idleResult) return idleResult;
537
+
538
+ // 액션 필요 없음
539
+ return this.buildAnalysis(StallType.IDLE, RecoveryAction.NONE, 'Monitoring', true);
540
+ }
541
+
542
+ /** 분석 컨텍스트 생성 */
543
+ private buildAnalysisContext(state: LaneStallState): AnalysisContext {
544
+ const now = Date.now();
545
+ return {
546
+ state,
547
+ idleTime: now - state.lastRealActivityTime,
548
+ progressTime: now - state.lastStateUpdateTime,
549
+ taskTime: now - state.taskStartTime,
550
+ timeSincePhaseChange: now - state.lastPhaseChangeTime,
551
+ bytesDelta: state.totalBytesReceived - state.bytesAtLastCheck,
552
+ effectiveIdleTimeout: state.isLongOperation
553
+ ? this.config.longOperationGraceMs
554
+ : this.config.idleTimeoutMs,
555
+ };
556
+ }
557
+
558
+ /** Task timeout 체크 */
559
+ private checkTaskTimeout(ctx: AnalysisContext): StallAnalysis | null {
560
+ if (ctx.taskTime <= this.config.taskTimeoutMs) return null;
561
+
562
+ const canRestart = ctx.state.restartCount < this.config.maxRestarts;
563
+ return this.buildAnalysis(
564
+ StallType.TASK_TIMEOUT,
565
+ canRestart ? RecoveryAction.REQUEST_RESTART : RecoveryAction.RUN_DOCTOR,
566
+ `Task exceeded maximum timeout of ${Math.round(this.config.taskTimeoutMs / 60000)} minutes`,
567
+ canRestart,
568
+ { taskTimeMs: ctx.taskTime, restartCount: ctx.state.restartCount }
569
+ );
570
+ }
571
+
572
+ /** Zero bytes 체크 (grace period 존중) */
573
+ private checkZeroBytes(ctx: AnalysisContext): StallAnalysis | null {
574
+ const { state, idleTime, timeSincePhaseChange, bytesDelta, effectiveIdleTimeout } = ctx;
575
+
576
+ if (bytesDelta !== 0 || idleTime <= effectiveIdleTimeout) return null;
577
+
578
+ const baseDetails = { idleTimeMs: idleTime, bytesDelta, phase: state.phase, timeSincePhaseChange };
579
+
580
+ switch (state.phase) {
581
+ case StallPhase.NORMAL:
582
+ return this.buildAnalysis(
583
+ StallType.ZERO_BYTES,
584
+ RecoveryAction.SEND_CONTINUE,
585
+ `Agent produced 0 bytes for ${Math.round(idleTime / 1000)}s - possible API issue`,
586
+ true, baseDetails
587
+ );
588
+
589
+ case StallPhase.CONTINUE_SENT:
590
+ if (timeSincePhaseChange > this.config.continueGraceMs) {
591
+ return this.buildAnalysis(
592
+ StallType.ZERO_BYTES,
593
+ RecoveryAction.SEND_STRONGER_PROMPT,
594
+ `Still 0 bytes after continue signal (${Math.round(timeSincePhaseChange / 1000)}s). Escalating...`,
595
+ true, baseDetails
596
+ );
597
+ }
598
+ return null; // Grace period 내 → 대기
599
+
600
+ case StallPhase.STRONGER_PROMPT_SENT:
601
+ if (timeSincePhaseChange > this.config.strongerPromptGraceMs) {
602
+ return this.buildAnalysis(
603
+ StallType.ZERO_BYTES,
604
+ RecoveryAction.REQUEST_RESTART,
605
+ `Still 0 bytes after stronger prompt (${Math.round(timeSincePhaseChange / 1000)}s). Restarting...`,
606
+ true, baseDetails
607
+ );
608
+ }
609
+ return null; // Grace period 내 → 대기
610
+
611
+ default:
612
+ // RESTART_REQUESTED, DIAGNOSED, ABORTED
613
+ return this.buildAnalysis(
614
+ StallType.ZERO_BYTES,
615
+ RecoveryAction.REQUEST_RESTART,
616
+ `Agent produced 0 bytes for ${Math.round(idleTime / 1000)}s - possible API issue`,
617
+ true, baseDetails
618
+ );
486
619
  }
620
+ }
621
+
622
+ /** Progress timeout 체크 */
623
+ private checkProgressTimeout(ctx: AnalysisContext): StallAnalysis | null {
624
+ if (ctx.progressTime <= this.config.progressTimeoutMs) return null;
625
+ return this.getEscalatedAction(ctx.state, StallType.NO_PROGRESS, ctx.progressTime);
626
+ }
627
+
628
+ /** Phase별 idle 상태 체크 */
629
+ private checkPhaseBasedIdle(ctx: AnalysisContext): StallAnalysis | null {
630
+ const { state, idleTime, timeSincePhaseChange, effectiveIdleTimeout } = ctx;
487
631
 
488
- // 4. Phase별 상태 체크
489
632
  switch (state.phase) {
490
633
  case StallPhase.NORMAL:
491
- // Idle timeout 체크
492
634
  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
- };
635
+ return this.buildAnalysis(
636
+ StallType.IDLE,
637
+ RecoveryAction.SEND_CONTINUE,
638
+ `Lane idle for ${Math.round(idleTime / 1000)}s. Sending continue signal...`,
639
+ true,
640
+ { idleTimeMs: idleTime, isLongOperation: state.isLongOperation }
641
+ );
500
642
  }
501
643
  break;
502
644
 
503
645
  case StallPhase.CONTINUE_SENT:
504
- // Continue 신호 후 유예 시간 초과
505
646
  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
- };
647
+ return this.buildAnalysis(
648
+ StallType.IDLE,
649
+ RecoveryAction.SEND_STRONGER_PROMPT,
650
+ `Still idle after continue signal. Sending stronger prompt...`,
651
+ true,
652
+ { timeSincePhaseChange, continueSignalCount: state.continueSignalCount }
653
+ );
513
654
  }
514
655
  break;
515
656
 
516
657
  case StallPhase.STRONGER_PROMPT_SENT:
517
- // Stronger prompt 후 유예 시간 초과
518
658
  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
- }
659
+ return this.buildRestartOrDoctorAnalysis(state,
660
+ 'No response after stronger prompt. Killing and restarting process...',
661
+ `Lane failed after ${state.restartCount} restarts. Running diagnostics...`
662
+ );
536
663
  }
537
664
  break;
538
665
 
539
666
  case StallPhase.RESTART_REQUESTED:
540
- // 재시작 후 idle timeout의 75%로 더 짧게 감지
541
667
  const postRestartTimeout = effectiveIdleTimeout * 0.75;
542
668
  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
- }
669
+ return this.buildRestartOrDoctorAnalysis(state,
670
+ 'Lane idle after restart. Retrying continue signal...',
671
+ 'Lane repeatedly stalled. Running diagnostics...',
672
+ RecoveryAction.SEND_CONTINUE // restart 후에는 continue부터 시작
673
+ );
560
674
  }
561
675
  break;
562
676
 
563
677
  case StallPhase.DIAGNOSED:
564
678
  case StallPhase.ABORTED:
565
- // 이상 복구 시도 안함
566
- return {
567
- type: StallType.IDLE,
568
- action: RecoveryAction.ABORT_LANE,
569
- message: 'Lane recovery exhausted',
570
- isTransient: false,
571
- };
679
+ return this.buildAnalysis(StallType.IDLE, RecoveryAction.ABORT_LANE, 'Lane recovery exhausted', false);
572
680
  }
573
681
 
574
- // 액션 필요 없음
575
- return {
576
- type: StallType.IDLE,
577
- action: RecoveryAction.NONE,
578
- message: 'Monitoring',
579
- isTransient: true,
580
- };
682
+ return null;
581
683
  }
582
684
 
583
- /**
584
- * Progress timeout에 대한 에스컬레이션 액션 결정
585
- */
685
+ /** 재시작 또는 Doctor 실행 결정 헬퍼 */
686
+ private buildRestartOrDoctorAnalysis(
687
+ state: LaneStallState,
688
+ restartMsg: string,
689
+ doctorMsg: string,
690
+ restartAction: RecoveryAction = RecoveryAction.REQUEST_RESTART
691
+ ): StallAnalysis {
692
+ const canRestart = state.restartCount < this.config.maxRestarts;
693
+ return this.buildAnalysis(
694
+ StallType.IDLE,
695
+ canRestart ? restartAction : RecoveryAction.RUN_DOCTOR,
696
+ canRestart ? restartMsg : doctorMsg,
697
+ canRestart,
698
+ { restartCount: state.restartCount, maxRestarts: this.config.maxRestarts }
699
+ );
700
+ }
701
+
702
+ /** Progress timeout에 대한 에스컬레이션 액션 결정 */
586
703
  private getEscalatedAction(state: LaneStallState, type: StallType, progressTime: number): StallAnalysis {
704
+ const details = { progressTimeMs: progressTime };
705
+
587
706
  switch (state.phase) {
588
707
  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
- };
708
+ return this.buildAnalysis(type, RecoveryAction.SEND_CONTINUE,
709
+ `No progress for ${Math.round(progressTime / 60000)} minutes. Sending continue signal...`,
710
+ true, details);
596
711
 
597
712
  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
- };
713
+ return this.buildAnalysis(type, RecoveryAction.SEND_STRONGER_PROMPT,
714
+ 'Still no progress. Sending stronger prompt...', true, details);
605
715
 
606
716
  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
- }
717
+ const canRestart = state.restartCount < this.config.maxRestarts;
718
+ return this.buildAnalysis(type,
719
+ canRestart ? RecoveryAction.REQUEST_RESTART : RecoveryAction.RUN_DOCTOR,
720
+ canRestart ? 'No progress after interventions. Restarting...' : 'Persistent no-progress state. Running diagnostics...',
721
+ canRestart,
722
+ { ...details, restartCount: state.restartCount }
723
+ );
624
724
  }
625
725
  }
626
726
 
@@ -690,6 +790,14 @@ export class StallDetectionService {
690
790
  * Continue 신호 발송
691
791
  */
692
792
  private sendContinueSignal(state: LaneStallState): void {
793
+ // Intervention이 비활성화된 경우 신호를 보내지 않고 phase만 업데이트
794
+ if (state.interventionEnabled === false) {
795
+ logger.warn(`[${state.laneName}] Continue signal skipped (intervention disabled). Stall will escalate on next check.`);
796
+ state.phase = StallPhase.CONTINUE_SENT;
797
+ state.lastPhaseChangeTime = Date.now();
798
+ return;
799
+ }
800
+
693
801
  if (!state.laneRunDir) {
694
802
  logger.error(`[StallService] [${state.laneName}] Cannot send continue signal: laneRunDir not set`);
695
803
  return;
@@ -720,6 +828,14 @@ export class StallDetectionService {
720
828
  * Stronger prompt 발송
721
829
  */
722
830
  private sendStrongerPrompt(state: LaneStallState): void {
831
+ // Intervention이 비활성화된 경우 신호를 보내지 않고 phase만 업데이트
832
+ if (state.interventionEnabled === false) {
833
+ logger.warn(`[${state.laneName}] Stronger prompt skipped (intervention disabled). Will escalate to restart.`);
834
+ state.phase = StallPhase.STRONGER_PROMPT_SENT;
835
+ state.lastPhaseChangeTime = Date.now();
836
+ return;
837
+ }
838
+
723
839
  if (!state.laneRunDir) {
724
840
  logger.error(`[StallService] [${state.laneName}] Cannot send stronger prompt: laneRunDir not set`);
725
841
  return;
@@ -140,11 +140,21 @@ 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] padded to 18 chars inside brackets
153
+ const lIdx = laneIndex ?? 1;
154
+ const tIdx = taskIndex ?? 1;
155
+ const combined = `${lIdx}-${tIdx}-${laneName}`;
156
+ const label = combined.substring(0, 18).padEnd(18);
157
+ const laneLabel = `${COLORS.magenta}[${label}]${COLORS.reset}`;
148
158
  const output = isError ? `${COLORS.red}${message}${COLORS.reset}` : message;
149
159
 
150
160
  if (isError) {
@@ -7,6 +7,7 @@
7
7
  * - Box format only for: user, assistant, system, result
8
8
  * - Compact format for: tool, tool_result, thinking (gray/dim)
9
9
  * - Tool names simplified: ShellToolCall → Shell
10
+ * - Lane labels fixed 20 chars: [1-1-backend ]
10
11
  */
11
12
 
12
13
  import { COLORS } from './console';
@@ -64,10 +65,10 @@ export function formatMessageForConsole(
64
65
  : '';
65
66
  const tsPrefix = ts ? `${COLORS.gray}[${ts}]${COLORS.reset} ` : '';
66
67
 
67
- // Lane label max 16 chars
68
- const truncatedLabel = laneLabel.length > 16 ? laneLabel.substring(0, 16) : laneLabel;
68
+ // Lane label max 20 chars
69
+ const truncatedLabel = laneLabel.length > 20 ? laneLabel.substring(0, 20) : laneLabel;
69
70
  const labelPrefix = truncatedLabel
70
- ? `${COLORS.magenta}${truncatedLabel.padEnd(16)}${COLORS.reset} `
71
+ ? `${COLORS.magenta}${truncatedLabel.padEnd(20)}${COLORS.reset} `
71
72
  : '';
72
73
 
73
74
  // Determine if should use box format
@@ -231,6 +232,8 @@ function getTypeInfo(type: MessageType): { label: string; color: string } {
231
232
  info: { label: 'INFO ', color: COLORS.cyan },
232
233
  warn: { label: 'WARN ', color: COLORS.yellow },
233
234
  error: { label: 'ERROR ', color: COLORS.red },
235
+ debug: { label: 'DEBUG ', color: COLORS.gray },
236
+ progress: { label: 'PROG ', color: COLORS.blue || COLORS.cyan },
234
237
  stdout: { label: 'STDOUT', color: COLORS.white },
235
238
  stderr: { label: 'STDERR', color: COLORS.red },
236
239
  };
@@ -69,8 +69,10 @@ 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
+ /** Enable intervention feature (stdin piping for message injection) */
75
+ enableIntervention?: boolean;
74
76
  /** Enhanced logging configuration */
75
77
  enhancedLogging?: Partial<EnhancedLogConfig>;
76
78
  /** Default AI model for tasks (default: 'gemini-3-flash') */
@@ -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 {