@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.
Files changed (214) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/README.md +20 -16
  3. package/commands/cursorflow-init.md +0 -4
  4. package/dist/cli/logs.js +108 -9
  5. package/dist/cli/logs.js.map +1 -1
  6. package/dist/cli/models.js +20 -3
  7. package/dist/cli/models.js.map +1 -1
  8. package/dist/cli/monitor.d.ts +7 -10
  9. package/dist/cli/monitor.js +1088 -1240
  10. package/dist/cli/monitor.js.map +1 -1
  11. package/dist/cli/prepare.js +0 -1
  12. package/dist/cli/prepare.js.map +1 -1
  13. package/dist/cli/resume.js +23 -5
  14. package/dist/cli/resume.js.map +1 -1
  15. package/dist/cli/run.js +28 -9
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/cli/signal.d.ts +6 -1
  18. package/dist/cli/signal.js +94 -12
  19. package/dist/cli/signal.js.map +1 -1
  20. package/dist/cli/tasks.js +3 -46
  21. package/dist/cli/tasks.js.map +1 -1
  22. package/dist/core/agent-supervisor.d.ts +23 -0
  23. package/dist/core/agent-supervisor.js +42 -0
  24. package/dist/core/agent-supervisor.js.map +1 -0
  25. package/dist/core/auto-recovery.d.ts +2 -1
  26. package/dist/core/auto-recovery.js +6 -1
  27. package/dist/core/auto-recovery.js.map +1 -1
  28. package/dist/core/failure-policy.d.ts +0 -1
  29. package/dist/core/failure-policy.js +0 -1
  30. package/dist/core/failure-policy.js.map +1 -1
  31. package/dist/core/git-lifecycle-manager.d.ts +284 -0
  32. package/dist/core/git-lifecycle-manager.js +778 -0
  33. package/dist/core/git-lifecycle-manager.js.map +1 -0
  34. package/dist/core/git-pipeline-coordinator.d.ts +21 -0
  35. package/dist/core/git-pipeline-coordinator.js +205 -0
  36. package/dist/core/git-pipeline-coordinator.js.map +1 -0
  37. package/dist/core/intervention.d.ts +176 -0
  38. package/dist/core/intervention.js +424 -0
  39. package/dist/core/intervention.js.map +1 -0
  40. package/dist/core/lane-state-machine.d.ts +423 -0
  41. package/dist/core/lane-state-machine.js +890 -0
  42. package/dist/core/lane-state-machine.js.map +1 -0
  43. package/dist/core/orchestrator.d.ts +4 -1
  44. package/dist/core/orchestrator.js +38 -63
  45. package/dist/core/orchestrator.js.map +1 -1
  46. package/dist/core/runner/agent.d.ts +7 -1
  47. package/dist/core/runner/agent.js +45 -30
  48. package/dist/core/runner/agent.js.map +1 -1
  49. package/dist/core/runner/pipeline.js +283 -109
  50. package/dist/core/runner/pipeline.js.map +1 -1
  51. package/dist/core/runner/task.d.ts +4 -5
  52. package/dist/core/runner/task.js +6 -77
  53. package/dist/core/runner/task.js.map +1 -1
  54. package/dist/core/runner.js +11 -2
  55. package/dist/core/runner.js.map +1 -1
  56. package/dist/core/stall-detection.d.ts +27 -4
  57. package/dist/core/stall-detection.js +116 -28
  58. package/dist/core/stall-detection.js.map +1 -1
  59. package/dist/hooks/contexts/index.d.ts +104 -0
  60. package/dist/hooks/contexts/index.js +134 -0
  61. package/dist/hooks/contexts/index.js.map +1 -0
  62. package/dist/hooks/data-accessor.d.ts +86 -0
  63. package/dist/hooks/data-accessor.js +410 -0
  64. package/dist/hooks/data-accessor.js.map +1 -0
  65. package/dist/hooks/flow-controller.d.ts +136 -0
  66. package/dist/hooks/flow-controller.js +351 -0
  67. package/dist/hooks/flow-controller.js.map +1 -0
  68. package/dist/hooks/index.d.ts +68 -0
  69. package/dist/hooks/index.js +105 -0
  70. package/dist/hooks/index.js.map +1 -0
  71. package/dist/hooks/manager.d.ts +129 -0
  72. package/dist/hooks/manager.js +389 -0
  73. package/dist/hooks/manager.js.map +1 -0
  74. package/dist/hooks/types.d.ts +463 -0
  75. package/dist/hooks/types.js +45 -0
  76. package/dist/hooks/types.js.map +1 -0
  77. package/dist/services/logging/buffer.d.ts +2 -2
  78. package/dist/services/logging/buffer.js +95 -42
  79. package/dist/services/logging/buffer.js.map +1 -1
  80. package/dist/services/logging/console.js +8 -5
  81. package/dist/services/logging/console.js.map +1 -1
  82. package/dist/services/logging/formatter.d.ts +9 -3
  83. package/dist/services/logging/formatter.js +64 -17
  84. package/dist/services/logging/formatter.js.map +1 -1
  85. package/dist/services/logging/index.d.ts +0 -1
  86. package/dist/services/logging/index.js +0 -1
  87. package/dist/services/logging/index.js.map +1 -1
  88. package/dist/services/logging/paths.d.ts +8 -0
  89. package/dist/services/logging/paths.js +48 -0
  90. package/dist/services/logging/paths.js.map +1 -0
  91. package/dist/services/logging/raw-log.d.ts +6 -0
  92. package/dist/services/logging/raw-log.js +37 -0
  93. package/dist/services/logging/raw-log.js.map +1 -0
  94. package/dist/services/process/index.js +1 -1
  95. package/dist/services/process/index.js.map +1 -1
  96. package/dist/types/agent.d.ts +15 -0
  97. package/dist/types/config.d.ts +24 -1
  98. package/dist/types/event-categories.d.ts +601 -0
  99. package/dist/types/event-categories.js +233 -0
  100. package/dist/types/event-categories.js.map +1 -0
  101. package/dist/types/events.d.ts +0 -20
  102. package/dist/types/flow.d.ts +10 -6
  103. package/dist/types/index.d.ts +1 -1
  104. package/dist/types/index.js +17 -3
  105. package/dist/types/index.js.map +1 -1
  106. package/dist/types/lane.d.ts +1 -1
  107. package/dist/types/logging.d.ts +1 -1
  108. package/dist/types/task.d.ts +13 -2
  109. package/dist/ui/log-viewer.d.ts +3 -0
  110. package/dist/ui/log-viewer.js +3 -0
  111. package/dist/ui/log-viewer.js.map +1 -1
  112. package/dist/utils/config.js +15 -1
  113. package/dist/utils/config.js.map +1 -1
  114. package/dist/utils/cursor-agent.d.ts +11 -1
  115. package/dist/utils/cursor-agent.js +63 -16
  116. package/dist/utils/cursor-agent.js.map +1 -1
  117. package/dist/utils/enhanced-logger.d.ts +5 -1
  118. package/dist/utils/enhanced-logger.js +99 -20
  119. package/dist/utils/enhanced-logger.js.map +1 -1
  120. package/dist/utils/event-registry.d.ts +222 -0
  121. package/dist/utils/event-registry.js +463 -0
  122. package/dist/utils/event-registry.js.map +1 -0
  123. package/dist/utils/events.d.ts +1 -13
  124. package/dist/utils/events.js.map +1 -1
  125. package/dist/utils/flow.d.ts +10 -0
  126. package/dist/utils/flow.js +75 -0
  127. package/dist/utils/flow.js.map +1 -1
  128. package/dist/utils/git.d.ts +12 -1
  129. package/dist/utils/git.js +54 -1
  130. package/dist/utils/git.js.map +1 -1
  131. package/dist/utils/log-constants.d.ts +1 -0
  132. package/dist/utils/log-constants.js +2 -1
  133. package/dist/utils/log-constants.js.map +1 -1
  134. package/dist/utils/log-formatter.d.ts +3 -2
  135. package/dist/utils/log-formatter.js +11 -11
  136. package/dist/utils/log-formatter.js.map +1 -1
  137. package/dist/utils/logger.d.ts +11 -0
  138. package/dist/utils/logger.js +82 -3
  139. package/dist/utils/logger.js.map +1 -1
  140. package/dist/utils/repro-thinking-logs.js +0 -13
  141. package/dist/utils/repro-thinking-logs.js.map +1 -1
  142. package/dist/utils/run-service.js +1 -1
  143. package/dist/utils/run-service.js.map +1 -1
  144. package/examples/README.md +0 -2
  145. package/examples/demo-project/README.md +1 -2
  146. package/package.json +18 -28
  147. package/scripts/setup-security.sh +0 -1
  148. package/scripts/test-log-parser.ts +171 -0
  149. package/scripts/verify-change.sh +272 -0
  150. package/src/cli/logs.ts +121 -10
  151. package/src/cli/models.ts +20 -3
  152. package/src/cli/monitor.ts +1257 -1342
  153. package/src/cli/prepare.ts +0 -1
  154. package/src/cli/resume.ts +29 -5
  155. package/src/cli/run.ts +29 -11
  156. package/src/cli/signal.ts +115 -17
  157. package/src/cli/tasks.ts +2 -59
  158. package/src/core/agent-supervisor.ts +64 -0
  159. package/src/core/auto-recovery.ts +7 -1
  160. package/src/core/failure-policy.ts +0 -1
  161. package/src/core/git-lifecycle-manager.ts +1011 -0
  162. package/src/core/git-pipeline-coordinator.ts +221 -0
  163. package/src/core/intervention.ts +481 -0
  164. package/src/core/lane-state-machine.ts +1097 -0
  165. package/src/core/orchestrator.ts +45 -62
  166. package/src/core/runner/agent.ts +66 -33
  167. package/src/core/runner/pipeline.ts +318 -122
  168. package/src/core/runner/task.ts +12 -93
  169. package/src/core/runner.ts +12 -2
  170. package/src/core/stall-detection.ts +145 -28
  171. package/src/hooks/contexts/index.ts +256 -0
  172. package/src/hooks/data-accessor.ts +488 -0
  173. package/src/hooks/flow-controller.ts +425 -0
  174. package/src/hooks/index.ts +154 -0
  175. package/src/hooks/manager.ts +434 -0
  176. package/src/hooks/types.ts +544 -0
  177. package/src/services/logging/buffer.ts +104 -43
  178. package/src/services/logging/console.ts +9 -5
  179. package/src/services/logging/formatter.ts +74 -17
  180. package/src/services/logging/index.ts +0 -2
  181. package/src/services/logging/paths.ts +14 -0
  182. package/src/services/logging/raw-log.ts +43 -0
  183. package/src/services/process/index.ts +1 -1
  184. package/src/types/agent.ts +15 -0
  185. package/src/types/config.ts +25 -1
  186. package/src/types/event-categories.ts +663 -0
  187. package/src/types/events.ts +0 -25
  188. package/src/types/flow.ts +10 -6
  189. package/src/types/index.ts +50 -4
  190. package/src/types/lane.ts +1 -2
  191. package/src/types/logging.ts +2 -1
  192. package/src/types/task.ts +13 -2
  193. package/src/ui/log-viewer.ts +3 -0
  194. package/src/utils/config.ts +17 -1
  195. package/src/utils/cursor-agent.ts +68 -16
  196. package/src/utils/enhanced-logger.ts +106 -20
  197. package/src/utils/event-registry.ts +595 -0
  198. package/src/utils/events.ts +0 -16
  199. package/src/utils/flow.ts +84 -0
  200. package/src/utils/git.ts +59 -1
  201. package/src/utils/log-constants.ts +2 -1
  202. package/src/utils/log-formatter.ts +11 -12
  203. package/src/utils/logger.ts +49 -3
  204. package/src/utils/repro-thinking-logs.ts +0 -15
  205. package/src/utils/run-service.ts +1 -1
  206. package/dist/services/logging/file-writer.d.ts +0 -71
  207. package/dist/services/logging/file-writer.js +0 -516
  208. package/dist/services/logging/file-writer.js.map +0 -1
  209. package/dist/types/review.d.ts +0 -17
  210. package/dist/types/review.js +0 -6
  211. package/dist/types/review.js.map +0 -1
  212. package/scripts/ai-security-check.js +0 -233
  213. package/src/services/logging/file-writer.ts +0 -526
  214. 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,481 @@
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
+ /** 기존 intervention.txt 파일명 (호환성 유지) */
86
+ export const LEGACY_INTERVENTION_FILE = 'intervention.txt';
87
+
88
+ /** 프로세스 종료 대기 시간 (ms) */
89
+ const KILL_TIMEOUT_MS = 5000;
90
+
91
+ // ============================================================================
92
+ // Intervention Messages
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Continue 신호 메시지 생성
97
+ */
98
+ export function createContinueMessage(): string {
99
+ 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.`;
100
+ }
101
+
102
+ /**
103
+ * Stronger prompt 메시지 생성
104
+ */
105
+ export function createStrongerPromptMessage(): string {
106
+ return `[SYSTEM INTERVENTION] You appear to be stuck or unresponsive. Please:
107
+ 1. If you've completed the current task, summarize your work and proceed to the next task.
108
+ 2. If you encountered an error, describe it and attempt to resolve it.
109
+ 3. If you're waiting for something, explain what and continue with available work.
110
+ 4. If you encountered a git error, resolve it (pull/rebase/merge) and continue.
111
+
112
+ Respond immediately with your current status and next action.`;
113
+ }
114
+
115
+ /**
116
+ * 시스템 재시작 메시지 생성
117
+ */
118
+ export function createRestartMessage(reason: string): string {
119
+ 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.`;
120
+ }
121
+
122
+ /**
123
+ * 사용자 개입 메시지 래핑
124
+ */
125
+ export function wrapUserIntervention(message: string): string {
126
+ return `[USER INTERVENTION] ${message}`;
127
+ }
128
+
129
+ // ============================================================================
130
+ // Core Functions
131
+ // ============================================================================
132
+
133
+ /**
134
+ * 개입 요청 파일 경로 가져오기
135
+ */
136
+ export function getPendingInterventionPath(laneRunDir: string): string {
137
+ return safeJoin(laneRunDir, PENDING_INTERVENTION_FILE);
138
+ }
139
+
140
+ /**
141
+ * 기존 intervention.txt 경로 가져오기 (호환성)
142
+ */
143
+ export function getLegacyInterventionPath(laneRunDir: string): string {
144
+ return safeJoin(laneRunDir, LEGACY_INTERVENTION_FILE);
145
+ }
146
+
147
+ /**
148
+ * 개입 요청 생성 및 저장
149
+ *
150
+ * @param laneRunDir Lane 실행 디렉토리
151
+ * @param request 개입 요청 데이터
152
+ * @returns 저장된 파일 경로
153
+ */
154
+ export function createInterventionRequest(
155
+ laneRunDir: string,
156
+ request: Omit<InterventionRequest, 'timestamp'>
157
+ ): string {
158
+ const fullRequest: InterventionRequest = {
159
+ ...request,
160
+ timestamp: Date.now(),
161
+ priority: request.priority ?? 0,
162
+ };
163
+
164
+ const filePath = getPendingInterventionPath(laneRunDir);
165
+
166
+ // 기존 요청이 있으면 우선순위 비교
167
+ const existing = readPendingIntervention(laneRunDir);
168
+ if (existing && (existing.priority ?? 0) > (fullRequest.priority ?? 0)) {
169
+ logger.debug(`[Intervention] Existing request has higher priority, skipping`);
170
+ return filePath;
171
+ }
172
+
173
+ fs.writeFileSync(filePath, JSON.stringify(fullRequest, null, 2), 'utf8');
174
+ logger.debug(`[Intervention] Created request: ${filePath}`);
175
+
176
+ // 기존 intervention.txt에도 기록 (호환성 및 로깅용)
177
+ const legacyPath = getLegacyInterventionPath(laneRunDir);
178
+ fs.writeFileSync(legacyPath, fullRequest.message, 'utf8');
179
+
180
+ return filePath;
181
+ }
182
+
183
+ /**
184
+ * 대기 중인 개입 요청 읽기
185
+ */
186
+ export function readPendingIntervention(laneRunDir: string): InterventionRequest | null {
187
+ const filePath = getPendingInterventionPath(laneRunDir);
188
+
189
+ if (!fs.existsSync(filePath)) {
190
+ return null;
191
+ }
192
+
193
+ try {
194
+ const content = fs.readFileSync(filePath, 'utf8');
195
+ return JSON.parse(content) as InterventionRequest;
196
+ } catch (error) {
197
+ logger.warn(`[Intervention] Failed to read pending intervention: ${error}`);
198
+ return null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 대기 중인 개입 요청 삭제 (처리 완료 후)
204
+ */
205
+ export function clearPendingIntervention(laneRunDir: string): void {
206
+ const filePath = getPendingInterventionPath(laneRunDir);
207
+ const legacyPath = getLegacyInterventionPath(laneRunDir);
208
+
209
+ try {
210
+ if (fs.existsSync(filePath)) {
211
+ fs.unlinkSync(filePath);
212
+ }
213
+ if (fs.existsSync(legacyPath)) {
214
+ fs.unlinkSync(legacyPath);
215
+ }
216
+ } catch (error) {
217
+ // Ignore cleanup errors
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 대기 중인 개입 요청이 있는지 확인
223
+ */
224
+ export function hasPendingIntervention(laneRunDir: string): boolean {
225
+ return fs.existsSync(getPendingInterventionPath(laneRunDir));
226
+ }
227
+
228
+ // ============================================================================
229
+ // Process Control
230
+ // ============================================================================
231
+
232
+ /**
233
+ * PID로 프로세스 종료
234
+ *
235
+ * @param pid 종료할 프로세스 PID
236
+ * @param signal 종료 시그널 (기본: SIGTERM)
237
+ * @returns 성공 여부
238
+ */
239
+ export function killProcess(pid: number, signal: NodeJS.Signals = 'SIGTERM'): boolean {
240
+ try {
241
+ process.kill(pid, signal);
242
+ logger.info(`[Intervention] Sent ${signal} to process ${pid}`);
243
+ return true;
244
+ } catch (error: any) {
245
+ if (error.code === 'ESRCH') {
246
+ logger.debug(`[Intervention] Process ${pid} already terminated`);
247
+ return true; // Process doesn't exist, consider it killed
248
+ }
249
+ logger.error(`[Intervention] Failed to kill process ${pid}: ${error.message}`);
250
+ return false;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 프로세스가 살아있는지 확인
256
+ */
257
+ export function isProcessAlive(pid: number): boolean {
258
+ try {
259
+ process.kill(pid, 0); // Signal 0 = check existence
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 프로세스 종료 후 대기
268
+ *
269
+ * @param pid 종료할 프로세스 PID
270
+ * @param timeoutMs 최대 대기 시간
271
+ * @returns 종료 성공 여부
272
+ */
273
+ export async function killAndWait(pid: number, timeoutMs: number = KILL_TIMEOUT_MS): Promise<boolean> {
274
+ // 먼저 SIGTERM 시도
275
+ if (!killProcess(pid, 'SIGTERM')) {
276
+ return false;
277
+ }
278
+
279
+ // 종료 대기
280
+ const startTime = Date.now();
281
+ while (Date.now() - startTime < timeoutMs) {
282
+ if (!isProcessAlive(pid)) {
283
+ return true;
284
+ }
285
+ await new Promise(resolve => setTimeout(resolve, 100));
286
+ }
287
+
288
+ // SIGTERM이 안 먹히면 SIGKILL
289
+ logger.warn(`[Intervention] Process ${pid} didn't respond to SIGTERM, sending SIGKILL`);
290
+ killProcess(pid, 'SIGKILL');
291
+
292
+ // SIGKILL 후 잠시 대기
293
+ await new Promise(resolve => setTimeout(resolve, 500));
294
+ return !isProcessAlive(pid);
295
+ }
296
+
297
+ /**
298
+ * ChildProcess 종료
299
+ */
300
+ export async function killChildProcess(child: ChildProcess): Promise<boolean> {
301
+ if (!child.pid) {
302
+ return false;
303
+ }
304
+
305
+ if (child.killed) {
306
+ return true;
307
+ }
308
+
309
+ return killAndWait(child.pid);
310
+ }
311
+
312
+ // ============================================================================
313
+ // High-Level API
314
+ // ============================================================================
315
+
316
+ /**
317
+ * 즉각 개입 실행 - 프로세스 종료 및 개입 요청 생성
318
+ *
319
+ * 이 함수는 다음을 수행합니다:
320
+ * 1. 개입 요청 파일 생성 (pending-intervention.json)
321
+ * 2. 현재 프로세스 종료 (SIGTERM → SIGKILL)
322
+ * 3. 결과 반환 (Orchestrator가 resume 처리)
323
+ *
324
+ * @param laneRunDir Lane 실행 디렉토리
325
+ * @param request 개입 요청 데이터
326
+ * @param pid 종료할 프로세스 PID (없으면 state.json에서 읽음)
327
+ */
328
+ export async function executeIntervention(
329
+ laneRunDir: string,
330
+ request: Omit<InterventionRequest, 'timestamp'>,
331
+ pid?: number
332
+ ): Promise<InterventionResult> {
333
+ // 1. 대상 PID 확인
334
+ let targetPid = pid;
335
+ if (!targetPid) {
336
+ const statePath = safeJoin(laneRunDir, 'state.json');
337
+ if (fs.existsSync(statePath)) {
338
+ const state = loadState<LaneState>(statePath);
339
+ targetPid = state?.pid;
340
+ }
341
+ }
342
+
343
+ if (!targetPid) {
344
+ return {
345
+ success: false,
346
+ error: 'No process PID found to interrupt',
347
+ };
348
+ }
349
+
350
+ // 2. 프로세스가 살아있는지 확인
351
+ if (!isProcessAlive(targetPid)) {
352
+ logger.info(`[Intervention] Process ${targetPid} is not running, just creating request`);
353
+ const pendingFile = createInterventionRequest(laneRunDir, request);
354
+ return {
355
+ success: true,
356
+ pendingFile,
357
+ };
358
+ }
359
+
360
+ // 3. 개입 요청 파일 생성 (프로세스 종료 전에)
361
+ const pendingFile = createInterventionRequest(laneRunDir, request);
362
+
363
+ // 4. 프로세스 종료
364
+ const killed = await killAndWait(targetPid);
365
+
366
+ if (!killed) {
367
+ return {
368
+ success: false,
369
+ error: `Failed to kill process ${targetPid}`,
370
+ pendingFile,
371
+ };
372
+ }
373
+
374
+ logger.info(`[Intervention] Successfully interrupted process ${targetPid}`);
375
+
376
+ return {
377
+ success: true,
378
+ killedPid: targetPid,
379
+ pendingFile,
380
+ };
381
+ }
382
+
383
+ /**
384
+ * 사용자 개입 실행 (cursorflow signal 명령용)
385
+ */
386
+ export async function executeUserIntervention(
387
+ laneRunDir: string,
388
+ message: string,
389
+ pid?: number
390
+ ): Promise<InterventionResult> {
391
+ return executeIntervention(laneRunDir, {
392
+ type: InterventionType.USER_MESSAGE,
393
+ message: wrapUserIntervention(message),
394
+ source: 'user',
395
+ priority: 10, // 사용자 개입은 높은 우선순위
396
+ }, pid);
397
+ }
398
+
399
+ /**
400
+ * Continue 신호 실행 (stall-detection용)
401
+ */
402
+ export async function executeContinueSignal(
403
+ laneRunDir: string,
404
+ pid?: number
405
+ ): Promise<InterventionResult> {
406
+ return executeIntervention(laneRunDir, {
407
+ type: InterventionType.CONTINUE_SIGNAL,
408
+ message: createContinueMessage(),
409
+ source: 'stall-detector',
410
+ priority: 5,
411
+ }, pid);
412
+ }
413
+
414
+ /**
415
+ * Stronger prompt 실행 (stall-detection용)
416
+ */
417
+ export async function executeStrongerPrompt(
418
+ laneRunDir: string,
419
+ pid?: number
420
+ ): Promise<InterventionResult> {
421
+ return executeIntervention(laneRunDir, {
422
+ type: InterventionType.STRONGER_PROMPT,
423
+ message: createStrongerPromptMessage(),
424
+ source: 'stall-detector',
425
+ priority: 7,
426
+ }, pid);
427
+ }
428
+
429
+ /**
430
+ * Git 가이던스 실행
431
+ */
432
+ export async function executeGitGuidance(
433
+ laneRunDir: string,
434
+ guidance: string,
435
+ pid?: number
436
+ ): Promise<InterventionResult> {
437
+ return executeIntervention(laneRunDir, {
438
+ type: InterventionType.GIT_GUIDANCE,
439
+ message: guidance,
440
+ source: 'system',
441
+ priority: 8,
442
+ }, pid);
443
+ }
444
+
445
+ // ============================================================================
446
+ // Resume Integration
447
+ // ============================================================================
448
+
449
+ /**
450
+ * 개입 메시지를 포함한 resume 프롬프트 생성
451
+ *
452
+ * Runner가 resume 시 호출하여 개입 메시지를 프롬프트에 포함
453
+ */
454
+ export function buildResumePromptWithIntervention(
455
+ laneRunDir: string,
456
+ originalPrompt: string
457
+ ): { prompt: string; hadIntervention: boolean } {
458
+ const intervention = readPendingIntervention(laneRunDir);
459
+
460
+ if (!intervention) {
461
+ return { prompt: originalPrompt, hadIntervention: false };
462
+ }
463
+
464
+ // 개입 메시지를 프롬프트 앞에 추가
465
+ const combinedPrompt = `${intervention.message}\n\n---\n\nOriginal Task:\n${originalPrompt}`;
466
+
467
+ // 개입 요청 파일 삭제 (처리 완료)
468
+ clearPendingIntervention(laneRunDir);
469
+
470
+ logger.info(`[Intervention] Applied pending intervention (type: ${intervention.type})`);
471
+
472
+ return { prompt: combinedPrompt, hadIntervention: true };
473
+ }
474
+
475
+ /**
476
+ * 개입으로 인한 재시작인지 확인
477
+ */
478
+ export function isInterventionRestart(laneRunDir: string): boolean {
479
+ return hasPendingIntervention(laneRunDir);
480
+ }
481
+