@litmers/cursorflow-orchestrator 0.1.39 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -2
- package/README.md +20 -16
- package/commands/cursorflow-init.md +0 -4
- package/dist/cli/logs.js +108 -9
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/models.js +20 -3
- package/dist/cli/models.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -10
- package/dist/cli/monitor.js +1088 -1240
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +0 -1
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +23 -5
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +28 -9
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.d.ts +6 -1
- package/dist/cli/signal.js +94 -12
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/tasks.js +3 -46
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/agent-supervisor.d.ts +23 -0
- package/dist/core/agent-supervisor.js +42 -0
- package/dist/core/agent-supervisor.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +2 -1
- package/dist/core/auto-recovery.js +6 -1
- package/dist/core/auto-recovery.js.map +1 -1
- package/dist/core/failure-policy.d.ts +0 -1
- package/dist/core/failure-policy.js +0 -1
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/git-lifecycle-manager.d.ts +284 -0
- package/dist/core/git-lifecycle-manager.js +778 -0
- package/dist/core/git-lifecycle-manager.js.map +1 -0
- package/dist/core/git-pipeline-coordinator.d.ts +21 -0
- package/dist/core/git-pipeline-coordinator.js +205 -0
- package/dist/core/git-pipeline-coordinator.js.map +1 -0
- package/dist/core/intervention.d.ts +176 -0
- package/dist/core/intervention.js +424 -0
- package/dist/core/intervention.js.map +1 -0
- package/dist/core/lane-state-machine.d.ts +423 -0
- package/dist/core/lane-state-machine.js +890 -0
- package/dist/core/lane-state-machine.js.map +1 -0
- package/dist/core/orchestrator.d.ts +4 -1
- package/dist/core/orchestrator.js +38 -63
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +7 -1
- package/dist/core/runner/agent.js +45 -30
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/core/runner/pipeline.js +283 -109
- package/dist/core/runner/pipeline.js.map +1 -1
- package/dist/core/runner/task.d.ts +4 -5
- package/dist/core/runner/task.js +6 -77
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +11 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +27 -4
- package/dist/core/stall-detection.js +116 -28
- package/dist/core/stall-detection.js.map +1 -1
- package/dist/hooks/contexts/index.d.ts +104 -0
- package/dist/hooks/contexts/index.js +134 -0
- package/dist/hooks/contexts/index.js.map +1 -0
- package/dist/hooks/data-accessor.d.ts +86 -0
- package/dist/hooks/data-accessor.js +410 -0
- package/dist/hooks/data-accessor.js.map +1 -0
- package/dist/hooks/flow-controller.d.ts +136 -0
- package/dist/hooks/flow-controller.js +351 -0
- package/dist/hooks/flow-controller.js.map +1 -0
- package/dist/hooks/index.d.ts +68 -0
- package/dist/hooks/index.js +105 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/manager.d.ts +129 -0
- package/dist/hooks/manager.js +389 -0
- package/dist/hooks/manager.js.map +1 -0
- package/dist/hooks/types.d.ts +463 -0
- package/dist/hooks/types.js +45 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/services/logging/buffer.d.ts +2 -2
- package/dist/services/logging/buffer.js +95 -42
- package/dist/services/logging/buffer.js.map +1 -1
- package/dist/services/logging/console.js +8 -5
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +9 -3
- package/dist/services/logging/formatter.js +64 -17
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/services/logging/index.d.ts +0 -1
- package/dist/services/logging/index.js +0 -1
- package/dist/services/logging/index.js.map +1 -1
- package/dist/services/logging/paths.d.ts +8 -0
- package/dist/services/logging/paths.js +48 -0
- package/dist/services/logging/paths.js.map +1 -0
- package/dist/services/logging/raw-log.d.ts +6 -0
- package/dist/services/logging/raw-log.js +37 -0
- package/dist/services/logging/raw-log.js.map +1 -0
- package/dist/services/process/index.js +1 -1
- package/dist/services/process/index.js.map +1 -1
- package/dist/types/agent.d.ts +15 -0
- package/dist/types/config.d.ts +24 -1
- package/dist/types/event-categories.d.ts +601 -0
- package/dist/types/event-categories.js +233 -0
- package/dist/types/event-categories.js.map +1 -0
- package/dist/types/events.d.ts +0 -20
- package/dist/types/flow.d.ts +10 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +17 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +1 -1
- package/dist/types/logging.d.ts +1 -1
- package/dist/types/task.d.ts +13 -2
- package/dist/ui/log-viewer.d.ts +3 -0
- package/dist/ui/log-viewer.js +3 -0
- package/dist/ui/log-viewer.js.map +1 -1
- package/dist/utils/config.js +15 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.d.ts +11 -1
- package/dist/utils/cursor-agent.js +63 -16
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +5 -1
- package/dist/utils/enhanced-logger.js +99 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/event-registry.d.ts +222 -0
- package/dist/utils/event-registry.js +463 -0
- package/dist/utils/event-registry.js.map +1 -0
- package/dist/utils/events.d.ts +1 -13
- package/dist/utils/events.js.map +1 -1
- package/dist/utils/flow.d.ts +10 -0
- package/dist/utils/flow.js +75 -0
- package/dist/utils/flow.js.map +1 -1
- package/dist/utils/git.d.ts +12 -1
- package/dist/utils/git.js +54 -1
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log-constants.d.ts +1 -0
- package/dist/utils/log-constants.js +2 -1
- package/dist/utils/log-constants.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +3 -2
- package/dist/utils/log-formatter.js +11 -11
- package/dist/utils/log-formatter.js.map +1 -1
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +82 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/repro-thinking-logs.js +0 -13
- package/dist/utils/repro-thinking-logs.js.map +1 -1
- package/dist/utils/run-service.js +1 -1
- package/dist/utils/run-service.js.map +1 -1
- package/examples/README.md +0 -2
- package/examples/demo-project/README.md +1 -2
- package/package.json +18 -28
- package/scripts/setup-security.sh +0 -1
- package/scripts/test-log-parser.ts +171 -0
- package/scripts/verify-change.sh +272 -0
- package/src/cli/logs.ts +121 -10
- package/src/cli/models.ts +20 -3
- package/src/cli/monitor.ts +1257 -1342
- package/src/cli/prepare.ts +0 -1
- package/src/cli/resume.ts +29 -5
- package/src/cli/run.ts +29 -11
- package/src/cli/signal.ts +115 -17
- package/src/cli/tasks.ts +2 -59
- package/src/core/agent-supervisor.ts +64 -0
- package/src/core/auto-recovery.ts +7 -1
- package/src/core/failure-policy.ts +0 -1
- package/src/core/git-lifecycle-manager.ts +1011 -0
- package/src/core/git-pipeline-coordinator.ts +221 -0
- package/src/core/intervention.ts +481 -0
- package/src/core/lane-state-machine.ts +1097 -0
- package/src/core/orchestrator.ts +45 -62
- package/src/core/runner/agent.ts +66 -33
- package/src/core/runner/pipeline.ts +318 -122
- package/src/core/runner/task.ts +12 -93
- package/src/core/runner.ts +12 -2
- package/src/core/stall-detection.ts +145 -28
- package/src/hooks/contexts/index.ts +256 -0
- package/src/hooks/data-accessor.ts +488 -0
- package/src/hooks/flow-controller.ts +425 -0
- package/src/hooks/index.ts +154 -0
- package/src/hooks/manager.ts +434 -0
- package/src/hooks/types.ts +544 -0
- package/src/services/logging/buffer.ts +104 -43
- package/src/services/logging/console.ts +9 -5
- package/src/services/logging/formatter.ts +74 -17
- package/src/services/logging/index.ts +0 -2
- package/src/services/logging/paths.ts +14 -0
- package/src/services/logging/raw-log.ts +43 -0
- package/src/services/process/index.ts +1 -1
- package/src/types/agent.ts +15 -0
- package/src/types/config.ts +25 -1
- package/src/types/event-categories.ts +663 -0
- package/src/types/events.ts +0 -25
- package/src/types/flow.ts +10 -6
- package/src/types/index.ts +50 -4
- package/src/types/lane.ts +1 -2
- package/src/types/logging.ts +2 -1
- package/src/types/task.ts +13 -2
- package/src/ui/log-viewer.ts +3 -0
- package/src/utils/config.ts +17 -1
- package/src/utils/cursor-agent.ts +68 -16
- package/src/utils/enhanced-logger.ts +106 -20
- package/src/utils/event-registry.ts +595 -0
- package/src/utils/events.ts +0 -16
- package/src/utils/flow.ts +84 -0
- package/src/utils/git.ts +59 -1
- package/src/utils/log-constants.ts +2 -1
- package/src/utils/log-formatter.ts +11 -12
- package/src/utils/logger.ts +49 -3
- package/src/utils/repro-thinking-logs.ts +0 -15
- package/src/utils/run-service.ts +1 -1
- package/dist/services/logging/file-writer.d.ts +0 -71
- package/dist/services/logging/file-writer.js +0 -516
- package/dist/services/logging/file-writer.js.map +0 -1
- package/dist/types/review.d.ts +0 -17
- package/dist/types/review.js +0 -6
- package/dist/types/review.js.map +0 -1
- package/scripts/ai-security-check.js +0 -233
- package/src/services/logging/file-writer.ts +0 -526
- package/src/types/review.ts +0 -20
package/src/core/runner/task.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
1
|
import * as path from 'path';
|
|
3
2
|
import * as git from '../../utils/git';
|
|
4
3
|
import * as logger from '../../utils/logger';
|
|
5
4
|
import { events } from '../../utils/events';
|
|
6
5
|
import { safeJoin } from '../../utils/path';
|
|
7
6
|
import { appendLog, createConversationEntry } from '../../utils/state';
|
|
8
|
-
import { Task, RunnerConfig, TaskExecutionResult
|
|
9
|
-
import { loadState } from '../../utils/state';
|
|
7
|
+
import { Task, RunnerConfig, TaskExecutionResult } from '../../types';
|
|
10
8
|
import { waitForTaskDependencies as waitForDeps, DependencyWaitOptions } from '../../utils/dependency';
|
|
11
|
-
import {
|
|
12
|
-
cursorAgentSend,
|
|
13
|
-
extractDependencyRequest
|
|
14
|
-
} from './agent';
|
|
9
|
+
import { extractDependencyRequest } from './agent';
|
|
15
10
|
import {
|
|
16
11
|
wrapPrompt,
|
|
17
12
|
applyDependencyFilePermissions
|
|
@@ -23,6 +18,7 @@ import {
|
|
|
23
18
|
clearDependencyRequestFile,
|
|
24
19
|
DependencyResult
|
|
25
20
|
} from './utils';
|
|
21
|
+
import { AgentSupervisor } from '../agent-supervisor';
|
|
26
22
|
|
|
27
23
|
/**
|
|
28
24
|
* Wait for task-level dependencies to be completed by other lanes
|
|
@@ -56,74 +52,6 @@ export async function waitForTaskDependencies(
|
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
/**
|
|
60
|
-
* Merge branches from dependency lanes with safe merge and conflict pre-check
|
|
61
|
-
*/
|
|
62
|
-
export async function mergeDependencyBranches(deps: string[], runDir: string, worktreeDir: string, pipelineBranch: string): Promise<void> {
|
|
63
|
-
if (!deps || deps.length === 0) return;
|
|
64
|
-
|
|
65
|
-
const lanesRoot = path.dirname(runDir);
|
|
66
|
-
const lanesToMerge = new Set(deps.map(d => d.split(':')[0]!));
|
|
67
|
-
|
|
68
|
-
// Ensure we are on the pipeline branch before merging dependencies
|
|
69
|
-
logger.info(`🔄 Syncing with ${pipelineBranch} before merging dependencies`);
|
|
70
|
-
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
71
|
-
|
|
72
|
-
for (const laneName of lanesToMerge) {
|
|
73
|
-
const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
|
|
74
|
-
if (!fs.existsSync(depStatePath)) continue;
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const state = loadState<LaneState>(depStatePath);
|
|
78
|
-
if (!state?.pipelineBranch) continue;
|
|
79
|
-
|
|
80
|
-
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
81
|
-
|
|
82
|
-
// Ensure we have the latest
|
|
83
|
-
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
84
|
-
|
|
85
|
-
// Pre-check for conflicts before attempting merge
|
|
86
|
-
const conflictCheck = git.checkMergeConflict(state.pipelineBranch, { cwd: worktreeDir });
|
|
87
|
-
|
|
88
|
-
if (conflictCheck.willConflict) {
|
|
89
|
-
logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`);
|
|
90
|
-
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
91
|
-
|
|
92
|
-
// Emit event for potential auto-recovery or notification
|
|
93
|
-
events.emit('merge.conflict_detected', {
|
|
94
|
-
laneName,
|
|
95
|
-
targetBranch: state.pipelineBranch,
|
|
96
|
-
conflictingFiles: conflictCheck.conflictingFiles,
|
|
97
|
-
preCheck: true,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
throw new Error(`Pre-merge conflict check failed: ${conflictCheck.conflictingFiles.join(', ')}. Consider rebasing or resolving conflicts manually.`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Use safe merge with conflict detection
|
|
104
|
-
const mergeResult = git.safeMerge(state.pipelineBranch, {
|
|
105
|
-
cwd: worktreeDir,
|
|
106
|
-
noFf: true,
|
|
107
|
-
message: `chore: merge task dependency from ${laneName}`,
|
|
108
|
-
abortOnConflict: true,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (!mergeResult.success) {
|
|
112
|
-
if (mergeResult.conflict) {
|
|
113
|
-
logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
114
|
-
throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
115
|
-
}
|
|
116
|
-
throw new Error(mergeResult.error || 'Merge failed');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
logger.success(`✓ Merged ${laneName}`);
|
|
120
|
-
} catch (e) {
|
|
121
|
-
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
122
|
-
throw e;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
55
|
/**
|
|
128
56
|
* Run a single task
|
|
129
57
|
*/
|
|
@@ -137,6 +65,8 @@ export async function runTask({
|
|
|
137
65
|
chatId,
|
|
138
66
|
runDir,
|
|
139
67
|
runRoot,
|
|
68
|
+
agentSupervisor,
|
|
69
|
+
laneName,
|
|
140
70
|
}: {
|
|
141
71
|
task: Task;
|
|
142
72
|
config: RunnerConfig;
|
|
@@ -147,6 +77,8 @@ export async function runTask({
|
|
|
147
77
|
chatId: string;
|
|
148
78
|
runDir: string;
|
|
149
79
|
runRoot?: string;
|
|
80
|
+
agentSupervisor: AgentSupervisor;
|
|
81
|
+
laneName: string;
|
|
150
82
|
}): Promise<TaskExecutionResult> {
|
|
151
83
|
// Calculate runRoot if not provided (runDir is lanes/{laneName}/, runRoot is parent of lanes/)
|
|
152
84
|
const calculatedRunRoot = runRoot || path.dirname(path.dirname(runDir));
|
|
@@ -212,32 +144,20 @@ export async function runTask({
|
|
|
212
144
|
}));
|
|
213
145
|
|
|
214
146
|
logger.info('Sending prompt to agent...');
|
|
215
|
-
const
|
|
216
|
-
events.emit('agent.prompt_sent', {
|
|
217
|
-
taskName: task.name,
|
|
218
|
-
model,
|
|
219
|
-
promptLength: wrappedPrompt.length,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const r1 = await cursorAgentSend({
|
|
147
|
+
const r1 = await agentSupervisor.sendTaskPrompt({
|
|
223
148
|
workspaceDir: worktreeDir,
|
|
224
149
|
chatId,
|
|
225
150
|
prompt: wrappedPrompt,
|
|
226
151
|
model,
|
|
152
|
+
laneName,
|
|
227
153
|
signalDir: runDir,
|
|
228
154
|
timeout,
|
|
229
155
|
enableIntervention: config.enableIntervention,
|
|
230
156
|
outputFormat: config.agentOutputFormat,
|
|
231
157
|
taskName: task.name,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
events.emit('agent.response_received', {
|
|
236
|
-
taskName: task.name,
|
|
237
|
-
ok: r1.ok,
|
|
238
|
-
duration,
|
|
239
|
-
responseLength: r1.resultText?.length || 0,
|
|
240
|
-
error: r1.error,
|
|
158
|
+
browser: task.browser || config.browser,
|
|
159
|
+
autoApproveCommands: config.autoApproveCommands,
|
|
160
|
+
autoApproveMcps: config.autoApproveMcps,
|
|
241
161
|
});
|
|
242
162
|
|
|
243
163
|
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
|
|
@@ -304,4 +224,3 @@ export async function runTask({
|
|
|
304
224
|
status: 'FINISHED',
|
|
305
225
|
};
|
|
306
226
|
}
|
|
307
|
-
|
package/src/core/runner.ts
CHANGED
|
@@ -36,6 +36,8 @@ if (require.main === module) {
|
|
|
36
36
|
const startIdxIdx = args.indexOf('--start-index');
|
|
37
37
|
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
38
38
|
const worktreeDirIdx = args.indexOf('--worktree-dir');
|
|
39
|
+
const browser = args.includes('--browser');
|
|
40
|
+
const skipPreflight = args.includes('--skip-preflight');
|
|
39
41
|
|
|
40
42
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
41
43
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
@@ -75,6 +77,9 @@ if (require.main === module) {
|
|
|
75
77
|
if (forcedWorktreeDir) {
|
|
76
78
|
config.worktreeDir = forcedWorktreeDir;
|
|
77
79
|
}
|
|
80
|
+
if (browser) {
|
|
81
|
+
config.browser = true;
|
|
82
|
+
}
|
|
78
83
|
} catch (error: any) {
|
|
79
84
|
console.error(`Failed to load tasks file: ${error.message}`);
|
|
80
85
|
process.exit(1);
|
|
@@ -87,7 +92,12 @@ if (require.main === module) {
|
|
|
87
92
|
};
|
|
88
93
|
|
|
89
94
|
// Add agent output format default
|
|
90
|
-
config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'json';
|
|
95
|
+
config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
|
|
96
|
+
|
|
97
|
+
// Merge intervention and logging settings
|
|
98
|
+
config.enableIntervention = config.enableIntervention ?? globalConfig?.enableIntervention ?? true;
|
|
99
|
+
config.verboseGit = config.verboseGit ?? globalConfig?.verboseGit ?? false;
|
|
100
|
+
config.browser = config.browser ?? globalConfig?.browser ?? false;
|
|
91
101
|
|
|
92
102
|
// Handle process interruption to ensure cleanup
|
|
93
103
|
const handleSignal = (signal: string) => {
|
|
@@ -101,7 +111,7 @@ if (require.main === module) {
|
|
|
101
111
|
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
102
112
|
|
|
103
113
|
// Run tasks
|
|
104
|
-
runTasks(tasksFile, config, runDir, { startIndex })
|
|
114
|
+
runTasks(tasksFile, config, runDir, { startIndex, skipPreflight })
|
|
105
115
|
.then(() => {
|
|
106
116
|
process.exit(0);
|
|
107
117
|
})
|
|
@@ -18,6 +18,14 @@ import { ChildProcess } from 'child_process';
|
|
|
18
18
|
import * as logger from '../utils/logger';
|
|
19
19
|
import { events } from '../utils/events';
|
|
20
20
|
import { safeJoin } from '../utils/path';
|
|
21
|
+
import {
|
|
22
|
+
createInterventionRequest,
|
|
23
|
+
InterventionType,
|
|
24
|
+
createContinueMessage,
|
|
25
|
+
createStrongerPromptMessage,
|
|
26
|
+
createRestartMessage,
|
|
27
|
+
killAndWait,
|
|
28
|
+
} from './intervention';
|
|
21
29
|
|
|
22
30
|
// ============================================================================
|
|
23
31
|
// 설정 (Configuration)
|
|
@@ -142,6 +150,10 @@ export interface LaneStallState {
|
|
|
142
150
|
laneName: string;
|
|
143
151
|
/** 현재 복구 단계 */
|
|
144
152
|
phase: StallPhase;
|
|
153
|
+
/** Lane의 현재 상태 (waiting, running 등) - waiting 시 stall 분석 스킵 */
|
|
154
|
+
laneStatus?: string;
|
|
155
|
+
/** Intervention 활성화 여부 - false면 continue 신호 스킵 */
|
|
156
|
+
interventionEnabled?: boolean;
|
|
145
157
|
/** 마지막 실제 활동 시간 (bytes > 0) */
|
|
146
158
|
lastRealActivityTime: number;
|
|
147
159
|
/** 마지막 상태 변경 시간 (phase 변경) */
|
|
@@ -294,6 +306,7 @@ export class StallDetectionService {
|
|
|
294
306
|
laneRunDir?: string;
|
|
295
307
|
childProcess?: ChildProcess;
|
|
296
308
|
startIndex?: number;
|
|
309
|
+
interventionEnabled?: boolean;
|
|
297
310
|
} = {}
|
|
298
311
|
): void {
|
|
299
312
|
const now = Date.now();
|
|
@@ -301,6 +314,7 @@ export class StallDetectionService {
|
|
|
301
314
|
this.laneStates.set(laneName, {
|
|
302
315
|
laneName,
|
|
303
316
|
phase: StallPhase.NORMAL,
|
|
317
|
+
interventionEnabled: options.interventionEnabled ?? true, // default to true
|
|
304
318
|
lastRealActivityTime: now,
|
|
305
319
|
lastPhaseChangeTime: now,
|
|
306
320
|
lastStateUpdateTime: now,
|
|
@@ -318,7 +332,7 @@ export class StallDetectionService {
|
|
|
318
332
|
});
|
|
319
333
|
|
|
320
334
|
if (this.config.verbose) {
|
|
321
|
-
logger.debug(`[StallService] Lane registered: ${laneName}`);
|
|
335
|
+
logger.debug(`[StallService] Lane registered: ${laneName} (intervention: ${options.interventionEnabled ?? true})`);
|
|
322
336
|
}
|
|
323
337
|
}
|
|
324
338
|
|
|
@@ -360,6 +374,44 @@ export class StallDetectionService {
|
|
|
360
374
|
}
|
|
361
375
|
}
|
|
362
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Lane 상태 업데이트 (waiting, running 등)
|
|
379
|
+
* waiting 상태일 때는 stall 분석을 스킵함
|
|
380
|
+
*/
|
|
381
|
+
setLaneStatus(laneName: string, status: string): void {
|
|
382
|
+
const state = this.laneStates.get(laneName);
|
|
383
|
+
if (state) {
|
|
384
|
+
state.laneStatus = status;
|
|
385
|
+
|
|
386
|
+
// waiting 상태로 전환 시 타이머 리셋 (의존성 대기 시간을 stall로 간주하지 않음)
|
|
387
|
+
if (status === 'waiting') {
|
|
388
|
+
const now = Date.now();
|
|
389
|
+
state.lastRealActivityTime = now;
|
|
390
|
+
state.lastStateUpdateTime = now;
|
|
391
|
+
state.taskStartTime = now;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this.config.verbose) {
|
|
395
|
+
logger.debug(`[StallService] [${laneName}] Lane status updated: ${status}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Intervention 활성화 상태 설정
|
|
402
|
+
* false로 설정하면 continue 신호를 보내지 않음
|
|
403
|
+
*/
|
|
404
|
+
setInterventionEnabled(laneName: string, enabled: boolean): void {
|
|
405
|
+
const state = this.laneStates.get(laneName);
|
|
406
|
+
if (state) {
|
|
407
|
+
state.interventionEnabled = enabled;
|
|
408
|
+
|
|
409
|
+
if (this.config.verbose) {
|
|
410
|
+
logger.debug(`[StallService] [${laneName}] Intervention ${enabled ? 'enabled' : 'disabled'}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
363
415
|
// --------------------------------------------------------------------------
|
|
364
416
|
// 활동 기록 (Activity Recording)
|
|
365
417
|
// --------------------------------------------------------------------------
|
|
@@ -450,6 +502,7 @@ export class StallDetectionService {
|
|
|
450
502
|
* Stall 상태 분석 - 현재 상태에서 필요한 액션 결정
|
|
451
503
|
*
|
|
452
504
|
* 분석 우선순위:
|
|
505
|
+
* 0. waiting 상태면 스킵 (의존성 대기 중)
|
|
453
506
|
* 1. Task timeout (30분) → RESTART/DOCTOR
|
|
454
507
|
* 2. Zero bytes + idle → phase별 에스컬레이션
|
|
455
508
|
* 3. No progress (10분) → 단계별 에스컬레이션
|
|
@@ -461,6 +514,17 @@ export class StallDetectionService {
|
|
|
461
514
|
return this.buildAnalysis(StallType.IDLE, RecoveryAction.NONE, 'Lane not found', false);
|
|
462
515
|
}
|
|
463
516
|
|
|
517
|
+
// 0. waiting 상태는 stall 분석 스킵 (의존성 대기 중이므로 정상)
|
|
518
|
+
if (state.laneStatus === 'waiting') {
|
|
519
|
+
return this.buildAnalysis(
|
|
520
|
+
StallType.IDLE,
|
|
521
|
+
RecoveryAction.NONE,
|
|
522
|
+
'Waiting for dependencies',
|
|
523
|
+
true,
|
|
524
|
+
{ laneStatus: 'waiting' }
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
464
528
|
const ctx = this.buildAnalysisContext(state);
|
|
465
529
|
|
|
466
530
|
// 1. Task timeout (최우선)
|
|
@@ -676,8 +740,10 @@ export class StallDetectionService {
|
|
|
676
740
|
* Stall 체크 및 복구 액션 실행
|
|
677
741
|
*
|
|
678
742
|
* @returns 실행된 분석 결과 (orchestrator에서 추가 처리 필요시 사용)
|
|
743
|
+
*
|
|
744
|
+
* 새로운 방식에서는 복구 액션이 프로세스 중단을 포함하므로 async
|
|
679
745
|
*/
|
|
680
|
-
checkAndRecover(laneName: string): StallAnalysis {
|
|
746
|
+
async checkAndRecover(laneName: string): Promise<StallAnalysis> {
|
|
681
747
|
const state = this.laneStates.get(laneName);
|
|
682
748
|
if (!state) {
|
|
683
749
|
return {
|
|
@@ -704,18 +770,18 @@ export class StallDetectionService {
|
|
|
704
770
|
// 실패 이력 기록
|
|
705
771
|
this.recordFailure(state, analysis);
|
|
706
772
|
|
|
707
|
-
// 액션 실행
|
|
773
|
+
// 액션 실행 (프로세스 중단 포함 - await 필요)
|
|
708
774
|
switch (analysis.action) {
|
|
709
775
|
case RecoveryAction.SEND_CONTINUE:
|
|
710
|
-
this.sendContinueSignal(state);
|
|
776
|
+
await this.sendContinueSignal(state);
|
|
711
777
|
break;
|
|
712
778
|
|
|
713
779
|
case RecoveryAction.SEND_STRONGER_PROMPT:
|
|
714
|
-
this.sendStrongerPrompt(state);
|
|
780
|
+
await this.sendStrongerPrompt(state);
|
|
715
781
|
break;
|
|
716
782
|
|
|
717
783
|
case RecoveryAction.REQUEST_RESTART:
|
|
718
|
-
this.requestRestart(state);
|
|
784
|
+
await this.requestRestart(state);
|
|
719
785
|
break;
|
|
720
786
|
|
|
721
787
|
case RecoveryAction.RUN_DOCTOR:
|
|
@@ -731,24 +797,47 @@ export class StallDetectionService {
|
|
|
731
797
|
}
|
|
732
798
|
|
|
733
799
|
/**
|
|
734
|
-
* Continue 신호 발송
|
|
800
|
+
* Continue 신호 발송 - 프로세스 중단 및 개입 메시지와 함께 resume
|
|
801
|
+
*
|
|
802
|
+
* 새로운 방식:
|
|
803
|
+
* 1. pending-intervention.json 생성
|
|
804
|
+
* 2. 현재 프로세스 SIGTERM으로 종료
|
|
805
|
+
* 3. Orchestrator가 감지하여 개입 메시지와 함께 resume
|
|
735
806
|
*/
|
|
736
|
-
private sendContinueSignal(state: LaneStallState): void {
|
|
807
|
+
private async sendContinueSignal(state: LaneStallState): Promise<void> {
|
|
808
|
+
// Intervention이 비활성화된 경우 신호를 보내지 않고 phase만 업데이트
|
|
809
|
+
if (state.interventionEnabled === false) {
|
|
810
|
+
logger.warn(`[${state.laneName}] Continue signal skipped (intervention disabled). Stall will escalate on next check.`);
|
|
811
|
+
state.phase = StallPhase.CONTINUE_SENT;
|
|
812
|
+
state.lastPhaseChangeTime = Date.now();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
737
816
|
if (!state.laneRunDir) {
|
|
738
817
|
logger.error(`[StallService] [${state.laneName}] Cannot send continue signal: laneRunDir not set`);
|
|
739
818
|
return;
|
|
740
819
|
}
|
|
741
820
|
|
|
742
|
-
const interventionPath = safeJoin(state.laneRunDir, 'intervention.txt');
|
|
743
|
-
|
|
744
821
|
try {
|
|
745
|
-
|
|
822
|
+
// 1. 개입 요청 생성
|
|
823
|
+
createInterventionRequest(state.laneRunDir, {
|
|
824
|
+
type: InterventionType.CONTINUE_SIGNAL,
|
|
825
|
+
message: createContinueMessage(),
|
|
826
|
+
source: 'stall-detector',
|
|
827
|
+
priority: 5,
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// 2. 프로세스 종료 (있는 경우)
|
|
831
|
+
if (state.childProcess?.pid && !state.childProcess.killed) {
|
|
832
|
+
logger.info(`[${state.laneName}] Interrupting process ${state.childProcess.pid} for continue signal`);
|
|
833
|
+
await killAndWait(state.childProcess.pid);
|
|
834
|
+
}
|
|
746
835
|
|
|
747
836
|
state.phase = StallPhase.CONTINUE_SENT;
|
|
748
837
|
state.lastPhaseChangeTime = Date.now();
|
|
749
838
|
state.continueSignalCount++;
|
|
750
839
|
|
|
751
|
-
logger.info(`[${state.laneName}]
|
|
840
|
+
logger.info(`[${state.laneName}] Continue signal queued (#${state.continueSignalCount}) - agent will resume with intervention`);
|
|
752
841
|
|
|
753
842
|
events.emit('recovery.continue_signal', {
|
|
754
843
|
laneName: state.laneName,
|
|
@@ -761,28 +850,41 @@ export class StallDetectionService {
|
|
|
761
850
|
}
|
|
762
851
|
|
|
763
852
|
/**
|
|
764
|
-
* Stronger prompt 발송
|
|
853
|
+
* Stronger prompt 발송 - 프로세스 중단 및 강력한 개입 메시지와 함께 resume
|
|
765
854
|
*/
|
|
766
|
-
private sendStrongerPrompt(state: LaneStallState): void {
|
|
855
|
+
private async sendStrongerPrompt(state: LaneStallState): Promise<void> {
|
|
856
|
+
// Intervention이 비활성화된 경우 신호를 보내지 않고 phase만 업데이트
|
|
857
|
+
if (state.interventionEnabled === false) {
|
|
858
|
+
logger.warn(`[${state.laneName}] Stronger prompt skipped (intervention disabled). Will escalate to restart.`);
|
|
859
|
+
state.phase = StallPhase.STRONGER_PROMPT_SENT;
|
|
860
|
+
state.lastPhaseChangeTime = Date.now();
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
767
864
|
if (!state.laneRunDir) {
|
|
768
865
|
logger.error(`[StallService] [${state.laneName}] Cannot send stronger prompt: laneRunDir not set`);
|
|
769
866
|
return;
|
|
770
867
|
}
|
|
771
868
|
|
|
772
|
-
const interventionPath = safeJoin(state.laneRunDir, 'intervention.txt');
|
|
773
|
-
const prompt = `[SYSTEM INTERVENTION] You seem to be stuck or waiting.
|
|
774
|
-
Please continue with your current task immediately.
|
|
775
|
-
If you're waiting for something, explain what you need and proceed with what you can do now.
|
|
776
|
-
If you've completed the task, please summarize your work and finish.
|
|
777
|
-
If you encountered a git error, resolve it and continue.`;
|
|
778
|
-
|
|
779
869
|
try {
|
|
780
|
-
|
|
870
|
+
// 1. 개입 요청 생성
|
|
871
|
+
createInterventionRequest(state.laneRunDir, {
|
|
872
|
+
type: InterventionType.STRONGER_PROMPT,
|
|
873
|
+
message: createStrongerPromptMessage(),
|
|
874
|
+
source: 'stall-detector',
|
|
875
|
+
priority: 7,
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// 2. 프로세스 종료 (있는 경우)
|
|
879
|
+
if (state.childProcess?.pid && !state.childProcess.killed) {
|
|
880
|
+
logger.warn(`[${state.laneName}] Interrupting process ${state.childProcess.pid} for stronger prompt`);
|
|
881
|
+
await killAndWait(state.childProcess.pid);
|
|
882
|
+
}
|
|
781
883
|
|
|
782
884
|
state.phase = StallPhase.STRONGER_PROMPT_SENT;
|
|
783
885
|
state.lastPhaseChangeTime = Date.now();
|
|
784
886
|
|
|
785
|
-
logger.warn(`[${state.laneName}]
|
|
887
|
+
logger.warn(`[${state.laneName}] Stronger prompt queued - agent will resume with intervention`);
|
|
786
888
|
|
|
787
889
|
events.emit('recovery.stronger_prompt', {
|
|
788
890
|
laneName: state.laneName,
|
|
@@ -793,16 +895,31 @@ If you encountered a git error, resolve it and continue.`;
|
|
|
793
895
|
}
|
|
794
896
|
|
|
795
897
|
/**
|
|
796
|
-
* 재시작 요청
|
|
898
|
+
* 재시작 요청 - 프로세스 종료 및 재시작 메시지와 함께 resume
|
|
797
899
|
*/
|
|
798
|
-
private requestRestart(state: LaneStallState): void {
|
|
900
|
+
private async requestRestart(state: LaneStallState): Promise<void> {
|
|
799
901
|
state.restartCount++;
|
|
800
902
|
state.phase = StallPhase.RESTART_REQUESTED;
|
|
801
903
|
state.lastPhaseChangeTime = Date.now();
|
|
802
904
|
|
|
803
|
-
//
|
|
804
|
-
if (state.
|
|
905
|
+
// 1. 개입 요청 생성 (재시작 메시지)
|
|
906
|
+
if (state.laneRunDir) {
|
|
907
|
+
createInterventionRequest(state.laneRunDir, {
|
|
908
|
+
type: InterventionType.SYSTEM_RESTART,
|
|
909
|
+
message: createRestartMessage('Agent became unresponsive after multiple intervention attempts'),
|
|
910
|
+
source: 'stall-detector',
|
|
911
|
+
priority: 9,
|
|
912
|
+
metadata: {
|
|
913
|
+
restartCount: state.restartCount,
|
|
914
|
+
maxRestarts: this.config.maxRestarts,
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 2. 프로세스 종료 (SIGKILL 사용 - 강제 종료)
|
|
920
|
+
if (state.childProcess?.pid && !state.childProcess.killed) {
|
|
805
921
|
try {
|
|
922
|
+
// SIGKILL로 즉시 종료 (SIGTERM이 안 먹힐 수 있으므로)
|
|
806
923
|
state.childProcess.kill('SIGKILL');
|
|
807
924
|
logger.info(`[StallService] [${state.laneName}] Killed process ${state.childProcess.pid}`);
|
|
808
925
|
} catch (error: any) {
|
|
@@ -810,7 +927,7 @@ If you encountered a git error, resolve it and continue.`;
|
|
|
810
927
|
}
|
|
811
928
|
}
|
|
812
929
|
|
|
813
|
-
logger.warn(`[${state.laneName}]
|
|
930
|
+
logger.warn(`[${state.laneName}] Restart requested (restart #${state.restartCount}/${this.config.maxRestarts})`);
|
|
814
931
|
|
|
815
932
|
events.emit('recovery.restart', {
|
|
816
933
|
laneName: state.laneName,
|