@litmers/cursorflow-orchestrator 0.1.40 → 0.2.3
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 +8 -3
- package/commands/cursorflow-init.md +0 -4
- package/dist/cli/index.js +0 -6
- package/dist/cli/index.js.map +1 -1
- 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 +1103 -1239
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/resume.js +21 -1
- 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 +99 -13
- 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 +3 -117
- package/dist/core/auto-recovery.js +4 -482
- package/dist/core/auto-recovery.js.map +1 -1
- package/dist/core/failure-policy.d.ts +0 -53
- package/dist/core/failure-policy.js +7 -175
- 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 +170 -0
- package/dist/core/intervention.js +408 -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 +39 -65
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +7 -1
- package/dist/core/runner/agent.js +54 -36
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/core/runner/pipeline.js +283 -123
- 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 -80
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +8 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +11 -4
- package/dist/core/stall-detection.js +64 -27
- 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 +6 -1
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +9 -4
- package/dist/services/logging/formatter.js +64 -18
- 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 +22 -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 +12 -1
- 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 +10 -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 +98 -19
- 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/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 +2 -1
- package/dist/utils/log-formatter.js +10 -10
- 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 +13 -34
- 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/index.ts +0 -6
- package/src/cli/logs.ts +121 -10
- package/src/cli/models.ts +20 -3
- package/src/cli/monitor.ts +1273 -1342
- package/src/cli/resume.ts +27 -1
- package/src/cli/run.ts +29 -11
- package/src/cli/signal.ts +120 -18
- package/src/cli/tasks.ts +2 -59
- package/src/core/agent-supervisor.ts +64 -0
- package/src/core/auto-recovery.ts +14 -590
- package/src/core/failure-policy.ts +7 -229
- package/src/core/git-lifecycle-manager.ts +1011 -0
- package/src/core/git-pipeline-coordinator.ts +221 -0
- package/src/core/intervention.ts +463 -0
- package/src/core/lane-state-machine.ts +1097 -0
- package/src/core/orchestrator.ts +48 -64
- package/src/core/runner/agent.ts +77 -39
- package/src/core/runner/pipeline.ts +318 -138
- package/src/core/runner/task.ts +12 -97
- package/src/core/runner.ts +8 -2
- package/src/core/stall-detection.ts +74 -27
- 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 +7 -1
- package/src/services/logging/formatter.ts +74 -18
- 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 +23 -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 +12 -1
- package/src/ui/log-viewer.ts +3 -0
- package/src/utils/config.ts +11 -1
- package/src/utils/cursor-agent.ts +68 -16
- package/src/utils/enhanced-logger.ts +105 -19
- package/src/utils/event-registry.ts +595 -0
- package/src/utils/events.ts +0 -16
- package/src/utils/flow.ts +83 -0
- package/src/utils/log-constants.ts +2 -1
- package/src/utils/log-formatter.ts +10 -11
- 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/cli/prepare.d.ts +0 -7
- package/dist/cli/prepare.js +0 -690
- package/dist/cli/prepare.js.map +0 -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/cli/prepare.ts +0 -777
- package/src/services/logging/file-writer.ts +0 -526
- package/src/types/review.ts +0 -20
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as git from '../utils/git';
|
|
4
|
+
import * as logger from '../utils/logger';
|
|
5
|
+
import { events } from '../utils/events';
|
|
6
|
+
import { safeJoin } from '../utils/path';
|
|
7
|
+
import { loadState } from '../utils/state';
|
|
8
|
+
import { LaneState } from '../types';
|
|
9
|
+
|
|
10
|
+
export interface WorktreeSetupOptions {
|
|
11
|
+
worktreeDir: string;
|
|
12
|
+
pipelineBranch: string;
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
baseBranch: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class GitPipelineCoordinator {
|
|
18
|
+
async ensureWorktree(options: WorktreeSetupOptions): Promise<void> {
|
|
19
|
+
const { worktreeDir, pipelineBranch, repoRoot, baseBranch } = options;
|
|
20
|
+
const worktreeNeedsCreation = !fs.existsSync(worktreeDir);
|
|
21
|
+
const worktreeIsInvalid = !worktreeNeedsCreation && !git.isValidWorktree(worktreeDir);
|
|
22
|
+
|
|
23
|
+
if (worktreeIsInvalid) {
|
|
24
|
+
logger.warn(`⚠️ Directory exists but is not a valid worktree: ${worktreeDir}`);
|
|
25
|
+
logger.info(` Cleaning up invalid directory and recreating worktree...`);
|
|
26
|
+
try {
|
|
27
|
+
git.cleanupInvalidWorktreeDir(worktreeDir);
|
|
28
|
+
} catch (e: any) {
|
|
29
|
+
logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`);
|
|
30
|
+
throw new Error(`Cannot proceed: worktree directory is invalid and cleanup failed`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (worktreeNeedsCreation || worktreeIsInvalid) {
|
|
35
|
+
let retries = 3;
|
|
36
|
+
let lastError: Error | null = null;
|
|
37
|
+
|
|
38
|
+
while (retries > 0) {
|
|
39
|
+
try {
|
|
40
|
+
const worktreeParent = path.dirname(worktreeDir);
|
|
41
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
42
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
|
|
46
|
+
baseBranch,
|
|
47
|
+
cwd: repoRoot,
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
lastError = e;
|
|
52
|
+
retries--;
|
|
53
|
+
if (retries > 0) {
|
|
54
|
+
const delay = Math.floor(Math.random() * 1000) + 500;
|
|
55
|
+
logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (retries === 0 && lastError) {
|
|
62
|
+
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
logger.info(`Reusing existing worktree: ${worktreeDir}`);
|
|
66
|
+
try {
|
|
67
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
68
|
+
} catch (e) {
|
|
69
|
+
logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async mergeDependencyBranches(
|
|
75
|
+
deps: string[],
|
|
76
|
+
runDir: string,
|
|
77
|
+
worktreeDir: string,
|
|
78
|
+
pipelineBranch: string
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
if (!deps || deps.length === 0) return;
|
|
81
|
+
|
|
82
|
+
const lanesRoot = path.dirname(runDir);
|
|
83
|
+
const lanesToMerge = new Set(deps.map(d => d.split(':')[0]!));
|
|
84
|
+
|
|
85
|
+
logger.info(`🔄 Syncing with ${pipelineBranch} before merging dependencies`);
|
|
86
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
87
|
+
|
|
88
|
+
for (const laneName of lanesToMerge) {
|
|
89
|
+
const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
|
|
90
|
+
if (!fs.existsSync(depStatePath)) continue;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const state = loadState<LaneState>(depStatePath);
|
|
94
|
+
if (!state?.pipelineBranch) continue;
|
|
95
|
+
|
|
96
|
+
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
97
|
+
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
98
|
+
|
|
99
|
+
const remoteBranchRef = `origin/${state.pipelineBranch}`;
|
|
100
|
+
const conflictCheck = git.checkMergeConflict(remoteBranchRef, { cwd: worktreeDir });
|
|
101
|
+
|
|
102
|
+
if (conflictCheck.willConflict) {
|
|
103
|
+
logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`);
|
|
104
|
+
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
105
|
+
|
|
106
|
+
events.emit('merge.conflict_detected', {
|
|
107
|
+
laneName,
|
|
108
|
+
targetBranch: state.pipelineBranch,
|
|
109
|
+
conflictingFiles: conflictCheck.conflictingFiles,
|
|
110
|
+
preCheck: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Pre-merge conflict check failed: ${conflictCheck.conflictingFiles.join(', ')}. ` +
|
|
115
|
+
'Consider rebasing or resolving conflicts manually.'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const mergeResult = git.safeMerge(remoteBranchRef, {
|
|
120
|
+
cwd: worktreeDir,
|
|
121
|
+
noFf: true,
|
|
122
|
+
message: `chore: merge task dependency from ${laneName}`,
|
|
123
|
+
abortOnConflict: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!mergeResult.success) {
|
|
127
|
+
if (mergeResult.conflict) {
|
|
128
|
+
logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
129
|
+
throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
throw new Error(mergeResult.error || 'Merge failed');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
logger.success(`✓ Merged ${laneName}`);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
mergeTaskIntoPipeline({
|
|
143
|
+
taskName,
|
|
144
|
+
taskBranch,
|
|
145
|
+
pipelineBranch,
|
|
146
|
+
worktreeDir,
|
|
147
|
+
}: {
|
|
148
|
+
taskName: string;
|
|
149
|
+
taskBranch: string;
|
|
150
|
+
pipelineBranch: string;
|
|
151
|
+
worktreeDir: string;
|
|
152
|
+
}): void {
|
|
153
|
+
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
154
|
+
logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
|
|
155
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
156
|
+
|
|
157
|
+
const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
|
|
158
|
+
if (conflictCheck.willConflict) {
|
|
159
|
+
logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
|
|
160
|
+
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
161
|
+
logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
|
|
162
|
+
|
|
163
|
+
events.emit('merge.conflict_detected', {
|
|
164
|
+
taskName,
|
|
165
|
+
taskBranch,
|
|
166
|
+
pipelineBranch,
|
|
167
|
+
conflictingFiles: conflictCheck.conflictingFiles,
|
|
168
|
+
preCheck: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
logger.info(`🔀 Merging task ${taskName} (${taskBranch}) into ${pipelineBranch}`);
|
|
173
|
+
const mergeResult = git.safeMerge(taskBranch, {
|
|
174
|
+
cwd: worktreeDir,
|
|
175
|
+
noFf: true,
|
|
176
|
+
message: `chore: merge task ${taskName} into pipeline`,
|
|
177
|
+
abortOnConflict: true,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!mergeResult.success) {
|
|
181
|
+
if (mergeResult.conflict) {
|
|
182
|
+
logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Merge conflict when integrating task ${taskName}: ${mergeResult.conflictingFiles.join(', ')}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
throw new Error(mergeResult.error || 'Merge failed');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
191
|
+
if (stats) {
|
|
192
|
+
logger.info('Changed files:\n' + stats);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
finalizeFlowBranch({
|
|
197
|
+
flowBranch,
|
|
198
|
+
pipelineBranch,
|
|
199
|
+
worktreeDir,
|
|
200
|
+
}: {
|
|
201
|
+
flowBranch: string;
|
|
202
|
+
pipelineBranch: string;
|
|
203
|
+
worktreeDir: string;
|
|
204
|
+
}): void {
|
|
205
|
+
if (flowBranch === pipelineBranch) return;
|
|
206
|
+
|
|
207
|
+
logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
|
|
208
|
+
try {
|
|
209
|
+
git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
|
|
210
|
+
git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
|
|
211
|
+
|
|
212
|
+
logger.info(`🗑️ Deleting local pipeline branch: ${pipelineBranch}`);
|
|
213
|
+
git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
|
|
214
|
+
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
|
|
215
|
+
|
|
216
|
+
logger.success(`✓ Flow branch '${flowBranch}' created. Remote pipeline branch preserved for dependencies.`);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intervention Executor - 즉각적인 에이전트 개입을 위한 통합 모듈
|
|
3
|
+
*
|
|
4
|
+
* 핵심 기능:
|
|
5
|
+
* - 실행 중인 cursor-agent 프로세스 종료
|
|
6
|
+
* - 개입 메시지와 함께 세션 resume
|
|
7
|
+
* - signal, stall-detection에서 일관된 방식으로 사용
|
|
8
|
+
*
|
|
9
|
+
* 동작 원리:
|
|
10
|
+
* 1. 개입 요청 시 pending-intervention.json 파일 생성
|
|
11
|
+
* 2. 현재 프로세스 SIGTERM으로 종료
|
|
12
|
+
* 3. Orchestrator/Runner가 프로세스 종료 감지
|
|
13
|
+
* 4. pending-intervention.json 읽어서 개입 메시지 포함하여 resume
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { ChildProcess } from 'child_process';
|
|
19
|
+
import * as logger from '../utils/logger';
|
|
20
|
+
import { safeJoin } from '../utils/path';
|
|
21
|
+
import { loadState } from '../utils/state';
|
|
22
|
+
import { LaneState } from '../types';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 개입 요청 타입
|
|
30
|
+
*/
|
|
31
|
+
export enum InterventionType {
|
|
32
|
+
/** 사용자가 직접 보낸 메시지 */
|
|
33
|
+
USER_MESSAGE = 'user_message',
|
|
34
|
+
/** Stall 감지로 인한 continue 신호 */
|
|
35
|
+
CONTINUE_SIGNAL = 'continue_signal',
|
|
36
|
+
/** Stall 감지로 인한 stronger prompt */
|
|
37
|
+
STRONGER_PROMPT = 'stronger_prompt',
|
|
38
|
+
/** 시스템 재시작 요청 */
|
|
39
|
+
SYSTEM_RESTART = 'system_restart',
|
|
40
|
+
/** Git 에러 가이던스 */
|
|
41
|
+
GIT_GUIDANCE = 'git_guidance',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 개입 요청 데이터
|
|
46
|
+
*/
|
|
47
|
+
export interface InterventionRequest {
|
|
48
|
+
/** 개입 유형 */
|
|
49
|
+
type: InterventionType;
|
|
50
|
+
/** 개입 메시지 (에이전트에게 전달될 프롬프트) */
|
|
51
|
+
message: string;
|
|
52
|
+
/** 요청 시간 */
|
|
53
|
+
timestamp: number;
|
|
54
|
+
/** 요청자 (user, system, stall-detector) */
|
|
55
|
+
source: 'user' | 'system' | 'stall-detector';
|
|
56
|
+
/** 우선순위 (높을수록 먼저 처리) */
|
|
57
|
+
priority?: number;
|
|
58
|
+
/** 현재 태스크 인덱스 (resume 시 사용) */
|
|
59
|
+
taskIndex?: number;
|
|
60
|
+
/** 추가 메타데이터 */
|
|
61
|
+
metadata?: Record<string, any>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 개입 실행 결과
|
|
66
|
+
*/
|
|
67
|
+
export interface InterventionResult {
|
|
68
|
+
/** 성공 여부 */
|
|
69
|
+
success: boolean;
|
|
70
|
+
/** 종료된 프로세스 PID */
|
|
71
|
+
killedPid?: number;
|
|
72
|
+
/** 오류 메시지 */
|
|
73
|
+
error?: string;
|
|
74
|
+
/** pending-intervention.json 경로 */
|
|
75
|
+
pendingFile?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Constants
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
/** 개입 요청 파일명 */
|
|
83
|
+
export const PENDING_INTERVENTION_FILE = 'pending-intervention.json';
|
|
84
|
+
|
|
85
|
+
/** 프로세스 종료 대기 시간 (ms) */
|
|
86
|
+
const KILL_TIMEOUT_MS = 5000;
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Intervention Messages
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Continue 신호 메시지 생성
|
|
94
|
+
*/
|
|
95
|
+
export function createContinueMessage(): string {
|
|
96
|
+
return `[SYSTEM] Please continue with your current task. If you're waiting for something, explain what you need and proceed with what you can do now.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Stronger prompt 메시지 생성
|
|
101
|
+
*/
|
|
102
|
+
export function createStrongerPromptMessage(): string {
|
|
103
|
+
return `[SYSTEM INTERVENTION] You appear to be stuck or unresponsive. Please:
|
|
104
|
+
1. If you've completed the current task, summarize your work and proceed to the next task.
|
|
105
|
+
2. If you encountered an error, describe it and attempt to resolve it.
|
|
106
|
+
3. If you're waiting for something, explain what and continue with available work.
|
|
107
|
+
4. If you encountered a git error, resolve it (pull/rebase/merge) and continue.
|
|
108
|
+
|
|
109
|
+
Respond immediately with your current status and next action.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 시스템 재시작 메시지 생성
|
|
114
|
+
*/
|
|
115
|
+
export function createRestartMessage(reason: string): string {
|
|
116
|
+
return `[SYSTEM] Your previous session was interrupted due to: ${reason}. Please continue from where you left off. Review your progress and proceed with the current task.`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 사용자 개입 메시지 래핑
|
|
121
|
+
*/
|
|
122
|
+
export function wrapUserIntervention(message: string): string {
|
|
123
|
+
return `[USER INTERVENTION] ${message}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Core Functions
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 개입 요청 파일 경로 가져오기
|
|
132
|
+
*/
|
|
133
|
+
export function getPendingInterventionPath(laneRunDir: string): string {
|
|
134
|
+
return safeJoin(laneRunDir, PENDING_INTERVENTION_FILE);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 개입 요청 생성 및 저장
|
|
139
|
+
*
|
|
140
|
+
* @param laneRunDir Lane 실행 디렉토리
|
|
141
|
+
* @param request 개입 요청 데이터
|
|
142
|
+
* @returns 저장된 파일 경로
|
|
143
|
+
*/
|
|
144
|
+
export function createInterventionRequest(
|
|
145
|
+
laneRunDir: string,
|
|
146
|
+
request: Omit<InterventionRequest, 'timestamp'>
|
|
147
|
+
): string {
|
|
148
|
+
const fullRequest: InterventionRequest = {
|
|
149
|
+
...request,
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
priority: request.priority ?? 0,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const filePath = getPendingInterventionPath(laneRunDir);
|
|
155
|
+
|
|
156
|
+
// 기존 요청이 있으면 우선순위 비교
|
|
157
|
+
const existing = readPendingIntervention(laneRunDir);
|
|
158
|
+
if (existing && (existing.priority ?? 0) > (fullRequest.priority ?? 0)) {
|
|
159
|
+
logger.debug(`[Intervention] Existing request has higher priority, skipping`);
|
|
160
|
+
return filePath;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fs.writeFileSync(filePath, JSON.stringify(fullRequest, null, 2), 'utf8');
|
|
164
|
+
logger.debug(`[Intervention] Created request: ${filePath}`);
|
|
165
|
+
|
|
166
|
+
return filePath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 대기 중인 개입 요청 읽기
|
|
171
|
+
*/
|
|
172
|
+
export function readPendingIntervention(laneRunDir: string): InterventionRequest | null {
|
|
173
|
+
const filePath = getPendingInterventionPath(laneRunDir);
|
|
174
|
+
|
|
175
|
+
if (!fs.existsSync(filePath)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
181
|
+
return JSON.parse(content) as InterventionRequest;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logger.warn(`[Intervention] Failed to read pending intervention: ${error}`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 대기 중인 개입 요청 삭제 (처리 완료 후)
|
|
190
|
+
*/
|
|
191
|
+
export function clearPendingIntervention(laneRunDir: string): void {
|
|
192
|
+
const filePath = getPendingInterventionPath(laneRunDir);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
if (fs.existsSync(filePath)) {
|
|
196
|
+
fs.unlinkSync(filePath);
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// Ignore cleanup errors
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 대기 중인 개입 요청이 있는지 확인
|
|
205
|
+
*/
|
|
206
|
+
export function hasPendingIntervention(laneRunDir: string): boolean {
|
|
207
|
+
return fs.existsSync(getPendingInterventionPath(laneRunDir));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Process Control
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* PID로 프로세스 종료
|
|
216
|
+
*
|
|
217
|
+
* @param pid 종료할 프로세스 PID
|
|
218
|
+
* @param signal 종료 시그널 (기본: SIGTERM)
|
|
219
|
+
* @returns 성공 여부
|
|
220
|
+
*/
|
|
221
|
+
export function killProcess(pid: number, signal: NodeJS.Signals = 'SIGTERM'): boolean {
|
|
222
|
+
try {
|
|
223
|
+
process.kill(pid, signal);
|
|
224
|
+
logger.info(`[Intervention] Sent ${signal} to process ${pid}`);
|
|
225
|
+
return true;
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
if (error.code === 'ESRCH') {
|
|
228
|
+
logger.debug(`[Intervention] Process ${pid} already terminated`);
|
|
229
|
+
return true; // Process doesn't exist, consider it killed
|
|
230
|
+
}
|
|
231
|
+
logger.error(`[Intervention] Failed to kill process ${pid}: ${error.message}`);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 프로세스가 살아있는지 확인
|
|
238
|
+
*/
|
|
239
|
+
export function isProcessAlive(pid: number): boolean {
|
|
240
|
+
try {
|
|
241
|
+
process.kill(pid, 0); // Signal 0 = check existence
|
|
242
|
+
return true;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 프로세스 종료 후 대기
|
|
250
|
+
*
|
|
251
|
+
* @param pid 종료할 프로세스 PID
|
|
252
|
+
* @param timeoutMs 최대 대기 시간
|
|
253
|
+
* @returns 종료 성공 여부
|
|
254
|
+
*/
|
|
255
|
+
export async function killAndWait(pid: number, timeoutMs: number = KILL_TIMEOUT_MS): Promise<boolean> {
|
|
256
|
+
// 먼저 SIGTERM 시도
|
|
257
|
+
if (!killProcess(pid, 'SIGTERM')) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 종료 대기
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
264
|
+
if (!isProcessAlive(pid)) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// SIGTERM이 안 먹히면 SIGKILL
|
|
271
|
+
logger.warn(`[Intervention] Process ${pid} didn't respond to SIGTERM, sending SIGKILL`);
|
|
272
|
+
killProcess(pid, 'SIGKILL');
|
|
273
|
+
|
|
274
|
+
// SIGKILL 후 잠시 대기
|
|
275
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
276
|
+
return !isProcessAlive(pid);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* ChildProcess 종료
|
|
281
|
+
*/
|
|
282
|
+
export async function killChildProcess(child: ChildProcess): Promise<boolean> {
|
|
283
|
+
if (!child.pid) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (child.killed) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return killAndWait(child.pid);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ============================================================================
|
|
295
|
+
// High-Level API
|
|
296
|
+
// ============================================================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 즉각 개입 실행 - 프로세스 종료 및 개입 요청 생성
|
|
300
|
+
*
|
|
301
|
+
* 이 함수는 다음을 수행합니다:
|
|
302
|
+
* 1. 개입 요청 파일 생성 (pending-intervention.json)
|
|
303
|
+
* 2. 현재 프로세스 종료 (SIGTERM → SIGKILL)
|
|
304
|
+
* 3. 결과 반환 (Orchestrator가 resume 처리)
|
|
305
|
+
*
|
|
306
|
+
* @param laneRunDir Lane 실행 디렉토리
|
|
307
|
+
* @param request 개입 요청 데이터
|
|
308
|
+
* @param pid 종료할 프로세스 PID (없으면 state.json에서 읽음)
|
|
309
|
+
*/
|
|
310
|
+
export async function executeIntervention(
|
|
311
|
+
laneRunDir: string,
|
|
312
|
+
request: Omit<InterventionRequest, 'timestamp'>,
|
|
313
|
+
pid?: number
|
|
314
|
+
): Promise<InterventionResult> {
|
|
315
|
+
// 1. 대상 PID 확인
|
|
316
|
+
let targetPid = pid;
|
|
317
|
+
if (!targetPid) {
|
|
318
|
+
const statePath = safeJoin(laneRunDir, 'state.json');
|
|
319
|
+
if (fs.existsSync(statePath)) {
|
|
320
|
+
const state = loadState<LaneState>(statePath);
|
|
321
|
+
targetPid = state?.pid;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!targetPid) {
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
error: 'No process PID found to interrupt',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 2. 프로세스가 살아있는지 확인
|
|
333
|
+
if (!isProcessAlive(targetPid)) {
|
|
334
|
+
logger.info(`[Intervention] Process ${targetPid} is not running, just creating request`);
|
|
335
|
+
const pendingFile = createInterventionRequest(laneRunDir, request);
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
pendingFile,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 3. 개입 요청 파일 생성 (프로세스 종료 전에)
|
|
343
|
+
const pendingFile = createInterventionRequest(laneRunDir, request);
|
|
344
|
+
|
|
345
|
+
// 4. 프로세스 종료
|
|
346
|
+
const killed = await killAndWait(targetPid);
|
|
347
|
+
|
|
348
|
+
if (!killed) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
error: `Failed to kill process ${targetPid}`,
|
|
352
|
+
pendingFile,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
logger.info(`[Intervention] Successfully interrupted process ${targetPid}`);
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
success: true,
|
|
360
|
+
killedPid: targetPid,
|
|
361
|
+
pendingFile,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 사용자 개입 실행 (cursorflow signal 명령용)
|
|
367
|
+
*/
|
|
368
|
+
export async function executeUserIntervention(
|
|
369
|
+
laneRunDir: string,
|
|
370
|
+
message: string,
|
|
371
|
+
pid?: number
|
|
372
|
+
): Promise<InterventionResult> {
|
|
373
|
+
return executeIntervention(laneRunDir, {
|
|
374
|
+
type: InterventionType.USER_MESSAGE,
|
|
375
|
+
message: wrapUserIntervention(message),
|
|
376
|
+
source: 'user',
|
|
377
|
+
priority: 10, // 사용자 개입은 높은 우선순위
|
|
378
|
+
}, pid);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Continue 신호 실행 (stall-detection용)
|
|
383
|
+
*/
|
|
384
|
+
export async function executeContinueSignal(
|
|
385
|
+
laneRunDir: string,
|
|
386
|
+
pid?: number
|
|
387
|
+
): Promise<InterventionResult> {
|
|
388
|
+
return executeIntervention(laneRunDir, {
|
|
389
|
+
type: InterventionType.CONTINUE_SIGNAL,
|
|
390
|
+
message: createContinueMessage(),
|
|
391
|
+
source: 'stall-detector',
|
|
392
|
+
priority: 5,
|
|
393
|
+
}, pid);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Stronger prompt 실행 (stall-detection용)
|
|
398
|
+
*/
|
|
399
|
+
export async function executeStrongerPrompt(
|
|
400
|
+
laneRunDir: string,
|
|
401
|
+
pid?: number
|
|
402
|
+
): Promise<InterventionResult> {
|
|
403
|
+
return executeIntervention(laneRunDir, {
|
|
404
|
+
type: InterventionType.STRONGER_PROMPT,
|
|
405
|
+
message: createStrongerPromptMessage(),
|
|
406
|
+
source: 'stall-detector',
|
|
407
|
+
priority: 7,
|
|
408
|
+
}, pid);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Git 가이던스 실행
|
|
413
|
+
*/
|
|
414
|
+
export async function executeGitGuidance(
|
|
415
|
+
laneRunDir: string,
|
|
416
|
+
guidance: string,
|
|
417
|
+
pid?: number
|
|
418
|
+
): Promise<InterventionResult> {
|
|
419
|
+
return executeIntervention(laneRunDir, {
|
|
420
|
+
type: InterventionType.GIT_GUIDANCE,
|
|
421
|
+
message: guidance,
|
|
422
|
+
source: 'system',
|
|
423
|
+
priority: 8,
|
|
424
|
+
}, pid);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// Resume Integration
|
|
429
|
+
// ============================================================================
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 개입 메시지를 포함한 resume 프롬프트 생성
|
|
433
|
+
*
|
|
434
|
+
* Runner가 resume 시 호출하여 개입 메시지를 프롬프트에 포함
|
|
435
|
+
*/
|
|
436
|
+
export function buildResumePromptWithIntervention(
|
|
437
|
+
laneRunDir: string,
|
|
438
|
+
originalPrompt: string
|
|
439
|
+
): { prompt: string; hadIntervention: boolean } {
|
|
440
|
+
const intervention = readPendingIntervention(laneRunDir);
|
|
441
|
+
|
|
442
|
+
if (!intervention) {
|
|
443
|
+
return { prompt: originalPrompt, hadIntervention: false };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 개입 메시지를 프롬프트 앞에 추가
|
|
447
|
+
const combinedPrompt = `${intervention.message}\n\n---\n\nOriginal Task:\n${originalPrompt}`;
|
|
448
|
+
|
|
449
|
+
// 개입 요청 파일 삭제 (처리 완료)
|
|
450
|
+
clearPendingIntervention(laneRunDir);
|
|
451
|
+
|
|
452
|
+
logger.info(`[Intervention] Applied pending intervention (type: ${intervention.type})`);
|
|
453
|
+
|
|
454
|
+
return { prompt: combinedPrompt, hadIntervention: true };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 개입으로 인한 재시작인지 확인
|
|
459
|
+
*/
|
|
460
|
+
export function isInterventionRestart(laneRunDir: string): boolean {
|
|
461
|
+
return hasPendingIntervention(laneRunDir);
|
|
462
|
+
}
|
|
463
|
+
|