@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.
- package/README.md +13 -13
- 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/prepare.js +0 -1
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +24 -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/orchestrator.js +13 -5
- package/dist/core/orchestrator.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 +136 -173
- 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 +31 -40
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +15 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +32 -4
- package/dist/core/stall-detection.js +151 -149
- package/dist/core/stall-detection.js.map +1 -1
- package/dist/services/logging/console.d.ts +7 -1
- package/dist/services/logging/console.js +13 -3
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +1 -0
- package/dist/services/logging/formatter.js +6 -3
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/types/config.d.ts +3 -1
- package/dist/types/logging.d.ts +1 -1
- package/dist/types/task.d.ts +3 -8
- package/dist/utils/config.js +5 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +4 -4
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +1 -1
- package/dist/utils/enhanced-logger.js +3 -3
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +12 -1
- package/dist/utils/git.js +56 -1
- 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.d.ts +1 -1
- package/dist/utils/log-formatter.js +45 -8
- 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/prepare.ts +0 -1
- package/src/cli/resume.ts +28 -19
- package/src/cli/run.ts +1 -6
- package/src/cli/setup-commands.ts +1 -1
- package/src/core/orchestrator.ts +14 -5
- package/src/core/runner/agent.ts +36 -4
- package/src/core/runner/pipeline.ts +149 -182
- package/src/core/runner/prompt.ts +11 -18
- package/src/core/runner/task.ts +32 -41
- package/src/core/runner.ts +17 -2
- package/src/core/stall-detection.ts +263 -147
- package/src/services/logging/console.ts +13 -3
- package/src/services/logging/formatter.ts +6 -3
- package/src/types/config.ts +3 -1
- package/src/types/logging.ts +4 -2
- package/src/types/task.ts +3 -8
- package/src/utils/config.ts +6 -0
- package/src/utils/doctor.ts +5 -5
- package/src/utils/enhanced-logger.ts +3 -3
- package/src/utils/flow.ts +1 -0
- package/src/utils/git.ts +61 -1
- package/src/utils/health.ts +15 -15
- package/src/utils/log-formatter.ts +51 -8
- 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
package/src/core/runner.ts
CHANGED
|
@@ -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
|
|
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 →
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
454
|
-
|
|
455
|
-
: this.config.idleTimeoutMs;
|
|
522
|
+
// 1. Task timeout (최우선)
|
|
523
|
+
const taskResult = this.checkTaskTimeout(ctx);
|
|
524
|
+
if (taskResult) return taskResult;
|
|
456
525
|
|
|
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
|
-
}
|
|
526
|
+
// 2. Zero bytes + idle (에이전트 무응답)
|
|
527
|
+
const zeroByteResult = this.checkZeroBytes(ctx);
|
|
528
|
+
if (zeroByteResult) return zeroByteResult;
|
|
469
529
|
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
147
|
-
const
|
|
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
|
|
68
|
-
const truncatedLabel = laneLabel.length >
|
|
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(
|
|
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
|
};
|
package/src/types/config.ts
CHANGED
|
@@ -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') */
|