@litmers/cursorflow-orchestrator 0.1.40 → 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 (207) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/README.md +7 -3
  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/resume.js +21 -1
  12. package/dist/cli/resume.js.map +1 -1
  13. package/dist/cli/run.js +28 -9
  14. package/dist/cli/run.js.map +1 -1
  15. package/dist/cli/signal.d.ts +6 -1
  16. package/dist/cli/signal.js +94 -12
  17. package/dist/cli/signal.js.map +1 -1
  18. package/dist/cli/tasks.js +3 -46
  19. package/dist/cli/tasks.js.map +1 -1
  20. package/dist/core/agent-supervisor.d.ts +23 -0
  21. package/dist/core/agent-supervisor.js +42 -0
  22. package/dist/core/agent-supervisor.js.map +1 -0
  23. package/dist/core/auto-recovery.d.ts +2 -1
  24. package/dist/core/auto-recovery.js +6 -1
  25. package/dist/core/auto-recovery.js.map +1 -1
  26. package/dist/core/failure-policy.d.ts +0 -1
  27. package/dist/core/failure-policy.js +0 -1
  28. package/dist/core/failure-policy.js.map +1 -1
  29. package/dist/core/git-lifecycle-manager.d.ts +284 -0
  30. package/dist/core/git-lifecycle-manager.js +778 -0
  31. package/dist/core/git-lifecycle-manager.js.map +1 -0
  32. package/dist/core/git-pipeline-coordinator.d.ts +21 -0
  33. package/dist/core/git-pipeline-coordinator.js +205 -0
  34. package/dist/core/git-pipeline-coordinator.js.map +1 -0
  35. package/dist/core/intervention.d.ts +176 -0
  36. package/dist/core/intervention.js +424 -0
  37. package/dist/core/intervention.js.map +1 -0
  38. package/dist/core/lane-state-machine.d.ts +423 -0
  39. package/dist/core/lane-state-machine.js +890 -0
  40. package/dist/core/lane-state-machine.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +4 -1
  42. package/dist/core/orchestrator.js +29 -62
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +7 -1
  45. package/dist/core/runner/agent.js +45 -30
  46. package/dist/core/runner/agent.js.map +1 -1
  47. package/dist/core/runner/pipeline.js +283 -123
  48. package/dist/core/runner/pipeline.js.map +1 -1
  49. package/dist/core/runner/task.d.ts +4 -5
  50. package/dist/core/runner/task.js +6 -80
  51. package/dist/core/runner/task.js.map +1 -1
  52. package/dist/core/runner.js +8 -2
  53. package/dist/core/runner.js.map +1 -1
  54. package/dist/core/stall-detection.d.ts +11 -4
  55. package/dist/core/stall-detection.js +62 -27
  56. package/dist/core/stall-detection.js.map +1 -1
  57. package/dist/hooks/contexts/index.d.ts +104 -0
  58. package/dist/hooks/contexts/index.js +134 -0
  59. package/dist/hooks/contexts/index.js.map +1 -0
  60. package/dist/hooks/data-accessor.d.ts +86 -0
  61. package/dist/hooks/data-accessor.js +410 -0
  62. package/dist/hooks/data-accessor.js.map +1 -0
  63. package/dist/hooks/flow-controller.d.ts +136 -0
  64. package/dist/hooks/flow-controller.js +351 -0
  65. package/dist/hooks/flow-controller.js.map +1 -0
  66. package/dist/hooks/index.d.ts +68 -0
  67. package/dist/hooks/index.js +105 -0
  68. package/dist/hooks/index.js.map +1 -0
  69. package/dist/hooks/manager.d.ts +129 -0
  70. package/dist/hooks/manager.js +389 -0
  71. package/dist/hooks/manager.js.map +1 -0
  72. package/dist/hooks/types.d.ts +463 -0
  73. package/dist/hooks/types.js +45 -0
  74. package/dist/hooks/types.js.map +1 -0
  75. package/dist/services/logging/buffer.d.ts +2 -2
  76. package/dist/services/logging/buffer.js +95 -42
  77. package/dist/services/logging/buffer.js.map +1 -1
  78. package/dist/services/logging/console.js +6 -1
  79. package/dist/services/logging/console.js.map +1 -1
  80. package/dist/services/logging/formatter.d.ts +9 -4
  81. package/dist/services/logging/formatter.js +64 -18
  82. package/dist/services/logging/formatter.js.map +1 -1
  83. package/dist/services/logging/index.d.ts +0 -1
  84. package/dist/services/logging/index.js +0 -1
  85. package/dist/services/logging/index.js.map +1 -1
  86. package/dist/services/logging/paths.d.ts +8 -0
  87. package/dist/services/logging/paths.js +48 -0
  88. package/dist/services/logging/paths.js.map +1 -0
  89. package/dist/services/logging/raw-log.d.ts +6 -0
  90. package/dist/services/logging/raw-log.js +37 -0
  91. package/dist/services/logging/raw-log.js.map +1 -0
  92. package/dist/services/process/index.js +1 -1
  93. package/dist/services/process/index.js.map +1 -1
  94. package/dist/types/agent.d.ts +15 -0
  95. package/dist/types/config.d.ts +22 -1
  96. package/dist/types/event-categories.d.ts +601 -0
  97. package/dist/types/event-categories.js +233 -0
  98. package/dist/types/event-categories.js.map +1 -0
  99. package/dist/types/events.d.ts +0 -20
  100. package/dist/types/flow.d.ts +10 -6
  101. package/dist/types/index.d.ts +1 -1
  102. package/dist/types/index.js +17 -3
  103. package/dist/types/index.js.map +1 -1
  104. package/dist/types/lane.d.ts +1 -1
  105. package/dist/types/logging.d.ts +1 -1
  106. package/dist/types/task.d.ts +12 -1
  107. package/dist/ui/log-viewer.d.ts +3 -0
  108. package/dist/ui/log-viewer.js +3 -0
  109. package/dist/ui/log-viewer.js.map +1 -1
  110. package/dist/utils/config.js +10 -1
  111. package/dist/utils/config.js.map +1 -1
  112. package/dist/utils/cursor-agent.d.ts +11 -1
  113. package/dist/utils/cursor-agent.js +63 -16
  114. package/dist/utils/cursor-agent.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +5 -1
  116. package/dist/utils/enhanced-logger.js +98 -19
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/event-registry.d.ts +222 -0
  119. package/dist/utils/event-registry.js +463 -0
  120. package/dist/utils/event-registry.js.map +1 -0
  121. package/dist/utils/events.d.ts +1 -13
  122. package/dist/utils/events.js.map +1 -1
  123. package/dist/utils/flow.d.ts +10 -0
  124. package/dist/utils/flow.js +75 -0
  125. package/dist/utils/flow.js.map +1 -1
  126. package/dist/utils/log-constants.d.ts +1 -0
  127. package/dist/utils/log-constants.js +2 -1
  128. package/dist/utils/log-constants.js.map +1 -1
  129. package/dist/utils/log-formatter.d.ts +2 -1
  130. package/dist/utils/log-formatter.js +10 -10
  131. package/dist/utils/log-formatter.js.map +1 -1
  132. package/dist/utils/logger.d.ts +11 -0
  133. package/dist/utils/logger.js +82 -3
  134. package/dist/utils/logger.js.map +1 -1
  135. package/dist/utils/repro-thinking-logs.js +0 -13
  136. package/dist/utils/repro-thinking-logs.js.map +1 -1
  137. package/dist/utils/run-service.js +1 -1
  138. package/dist/utils/run-service.js.map +1 -1
  139. package/examples/README.md +0 -2
  140. package/examples/demo-project/README.md +1 -2
  141. package/package.json +18 -28
  142. package/scripts/setup-security.sh +0 -1
  143. package/scripts/test-log-parser.ts +171 -0
  144. package/scripts/verify-change.sh +272 -0
  145. package/src/cli/logs.ts +121 -10
  146. package/src/cli/models.ts +20 -3
  147. package/src/cli/monitor.ts +1257 -1342
  148. package/src/cli/resume.ts +27 -1
  149. package/src/cli/run.ts +29 -11
  150. package/src/cli/signal.ts +115 -17
  151. package/src/cli/tasks.ts +2 -59
  152. package/src/core/agent-supervisor.ts +64 -0
  153. package/src/core/auto-recovery.ts +7 -1
  154. package/src/core/failure-policy.ts +0 -1
  155. package/src/core/git-lifecycle-manager.ts +1011 -0
  156. package/src/core/git-pipeline-coordinator.ts +221 -0
  157. package/src/core/intervention.ts +481 -0
  158. package/src/core/lane-state-machine.ts +1097 -0
  159. package/src/core/orchestrator.ts +35 -61
  160. package/src/core/runner/agent.ts +66 -33
  161. package/src/core/runner/pipeline.ts +318 -138
  162. package/src/core/runner/task.ts +12 -97
  163. package/src/core/runner.ts +8 -2
  164. package/src/core/stall-detection.ts +72 -27
  165. package/src/hooks/contexts/index.ts +256 -0
  166. package/src/hooks/data-accessor.ts +488 -0
  167. package/src/hooks/flow-controller.ts +425 -0
  168. package/src/hooks/index.ts +154 -0
  169. package/src/hooks/manager.ts +434 -0
  170. package/src/hooks/types.ts +544 -0
  171. package/src/services/logging/buffer.ts +104 -43
  172. package/src/services/logging/console.ts +7 -1
  173. package/src/services/logging/formatter.ts +74 -18
  174. package/src/services/logging/index.ts +0 -2
  175. package/src/services/logging/paths.ts +14 -0
  176. package/src/services/logging/raw-log.ts +43 -0
  177. package/src/services/process/index.ts +1 -1
  178. package/src/types/agent.ts +15 -0
  179. package/src/types/config.ts +23 -1
  180. package/src/types/event-categories.ts +663 -0
  181. package/src/types/events.ts +0 -25
  182. package/src/types/flow.ts +10 -6
  183. package/src/types/index.ts +50 -4
  184. package/src/types/lane.ts +1 -2
  185. package/src/types/logging.ts +2 -1
  186. package/src/types/task.ts +12 -1
  187. package/src/ui/log-viewer.ts +3 -0
  188. package/src/utils/config.ts +11 -1
  189. package/src/utils/cursor-agent.ts +68 -16
  190. package/src/utils/enhanced-logger.ts +105 -19
  191. package/src/utils/event-registry.ts +595 -0
  192. package/src/utils/events.ts +0 -16
  193. package/src/utils/flow.ts +83 -0
  194. package/src/utils/log-constants.ts +2 -1
  195. package/src/utils/log-formatter.ts +10 -11
  196. package/src/utils/logger.ts +49 -3
  197. package/src/utils/repro-thinking-logs.ts +0 -15
  198. package/src/utils/run-service.ts +1 -1
  199. package/dist/services/logging/file-writer.d.ts +0 -71
  200. package/dist/services/logging/file-writer.js +0 -516
  201. package/dist/services/logging/file-writer.js.map +0 -1
  202. package/dist/types/review.d.ts +0 -17
  203. package/dist/types/review.js +0 -6
  204. package/dist/types/review.js.map +0 -1
  205. package/scripts/ai-security-check.js +0 -233
  206. package/src/services/logging/file-writer.ts +0 -526
  207. package/src/types/review.ts +0 -20
@@ -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, LaneState } from '../../types';
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,78 +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 from remote
83
- git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
84
-
85
- // Use the remote ref for merging (origin/<branch>) since dependency branches
86
- // are pushed to remote by other lanes and may not exist as local branches
87
- const remoteBranchRef = `origin/${state.pipelineBranch}`;
88
-
89
- // Pre-check for conflicts before attempting merge
90
- const conflictCheck = git.checkMergeConflict(remoteBranchRef, { cwd: worktreeDir });
91
-
92
- if (conflictCheck.willConflict) {
93
- logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`);
94
- logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
95
-
96
- // Emit event for potential auto-recovery or notification
97
- events.emit('merge.conflict_detected', {
98
- laneName,
99
- targetBranch: state.pipelineBranch,
100
- conflictingFiles: conflictCheck.conflictingFiles,
101
- preCheck: true,
102
- });
103
-
104
- throw new Error(`Pre-merge conflict check failed: ${conflictCheck.conflictingFiles.join(', ')}. Consider rebasing or resolving conflicts manually.`);
105
- }
106
-
107
- // Use safe merge with conflict detection - merge from remote ref
108
- const mergeResult = git.safeMerge(remoteBranchRef, {
109
- cwd: worktreeDir,
110
- noFf: true,
111
- message: `chore: merge task dependency from ${laneName}`,
112
- abortOnConflict: true,
113
- });
114
-
115
- if (!mergeResult.success) {
116
- if (mergeResult.conflict) {
117
- logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
118
- throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
119
- }
120
- throw new Error(mergeResult.error || 'Merge failed');
121
- }
122
-
123
- logger.success(`✓ Merged ${laneName}`);
124
- } catch (e) {
125
- logger.error(`Failed to merge branch from ${laneName}: ${e}`);
126
- throw e;
127
- }
128
- }
129
- }
130
-
131
55
  /**
132
56
  * Run a single task
133
57
  */
@@ -141,6 +65,8 @@ export async function runTask({
141
65
  chatId,
142
66
  runDir,
143
67
  runRoot,
68
+ agentSupervisor,
69
+ laneName,
144
70
  }: {
145
71
  task: Task;
146
72
  config: RunnerConfig;
@@ -151,6 +77,8 @@ export async function runTask({
151
77
  chatId: string;
152
78
  runDir: string;
153
79
  runRoot?: string;
80
+ agentSupervisor: AgentSupervisor;
81
+ laneName: string;
154
82
  }): Promise<TaskExecutionResult> {
155
83
  // Calculate runRoot if not provided (runDir is lanes/{laneName}/, runRoot is parent of lanes/)
156
84
  const calculatedRunRoot = runRoot || path.dirname(path.dirname(runDir));
@@ -216,32 +144,20 @@ export async function runTask({
216
144
  }));
217
145
 
218
146
  logger.info('Sending prompt to agent...');
219
- const startTime = Date.now();
220
- events.emit('agent.prompt_sent', {
221
- taskName: task.name,
222
- model,
223
- promptLength: wrappedPrompt.length,
224
- });
225
-
226
- const r1 = await cursorAgentSend({
147
+ const r1 = await agentSupervisor.sendTaskPrompt({
227
148
  workspaceDir: worktreeDir,
228
149
  chatId,
229
150
  prompt: wrappedPrompt,
230
151
  model,
152
+ laneName,
231
153
  signalDir: runDir,
232
154
  timeout,
233
155
  enableIntervention: config.enableIntervention,
234
156
  outputFormat: config.agentOutputFormat,
235
157
  taskName: task.name,
236
- });
237
-
238
- const duration = Date.now() - startTime;
239
- events.emit('agent.response_received', {
240
- taskName: task.name,
241
- ok: r1.ok,
242
- duration,
243
- responseLength: r1.resultText?.length || 0,
244
- error: r1.error,
158
+ browser: task.browser || config.browser,
159
+ autoApproveCommands: config.autoApproveCommands,
160
+ autoApproveMcps: config.autoApproveMcps,
245
161
  });
246
162
 
247
163
  appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
@@ -308,4 +224,3 @@ export async function runTask({
308
224
  status: 'FINISHED',
309
225
  };
310
226
  }
311
-
@@ -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,11 +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';
91
96
 
92
97
  // Merge intervention and logging settings
93
98
  config.enableIntervention = config.enableIntervention ?? globalConfig?.enableIntervention ?? true;
94
99
  config.verboseGit = config.verboseGit ?? globalConfig?.verboseGit ?? false;
100
+ config.browser = config.browser ?? globalConfig?.browser ?? false;
95
101
 
96
102
  // Handle process interruption to ensure cleanup
97
103
  const handleSignal = (signal: string) => {
@@ -105,7 +111,7 @@ if (require.main === module) {
105
111
  process.on('SIGTERM', () => handleSignal('SIGTERM'));
106
112
 
107
113
  // Run tasks
108
- runTasks(tasksFile, config, runDir, { startIndex })
114
+ runTasks(tasksFile, config, runDir, { startIndex, skipPreflight })
109
115
  .then(() => {
110
116
  process.exit(0);
111
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)
@@ -732,8 +740,10 @@ export class StallDetectionService {
732
740
  * Stall 체크 및 복구 액션 실행
733
741
  *
734
742
  * @returns 실행된 분석 결과 (orchestrator에서 추가 처리 필요시 사용)
743
+ *
744
+ * 새로운 방식에서는 복구 액션이 프로세스 중단을 포함하므로 async
735
745
  */
736
- checkAndRecover(laneName: string): StallAnalysis {
746
+ async checkAndRecover(laneName: string): Promise<StallAnalysis> {
737
747
  const state = this.laneStates.get(laneName);
738
748
  if (!state) {
739
749
  return {
@@ -760,18 +770,18 @@ export class StallDetectionService {
760
770
  // 실패 이력 기록
761
771
  this.recordFailure(state, analysis);
762
772
 
763
- // 액션 실행
773
+ // 액션 실행 (프로세스 중단 포함 - await 필요)
764
774
  switch (analysis.action) {
765
775
  case RecoveryAction.SEND_CONTINUE:
766
- this.sendContinueSignal(state);
776
+ await this.sendContinueSignal(state);
767
777
  break;
768
778
 
769
779
  case RecoveryAction.SEND_STRONGER_PROMPT:
770
- this.sendStrongerPrompt(state);
780
+ await this.sendStrongerPrompt(state);
771
781
  break;
772
782
 
773
783
  case RecoveryAction.REQUEST_RESTART:
774
- this.requestRestart(state);
784
+ await this.requestRestart(state);
775
785
  break;
776
786
 
777
787
  case RecoveryAction.RUN_DOCTOR:
@@ -787,9 +797,14 @@ export class StallDetectionService {
787
797
  }
788
798
 
789
799
  /**
790
- * Continue 신호 발송
800
+ * Continue 신호 발송 - 프로세스 중단 및 개입 메시지와 함께 resume
801
+ *
802
+ * 새로운 방식:
803
+ * 1. pending-intervention.json 생성
804
+ * 2. 현재 프로세스 SIGTERM으로 종료
805
+ * 3. Orchestrator가 감지하여 개입 메시지와 함께 resume
791
806
  */
792
- private sendContinueSignal(state: LaneStallState): void {
807
+ private async sendContinueSignal(state: LaneStallState): Promise<void> {
793
808
  // Intervention이 비활성화된 경우 신호를 보내지 않고 phase만 업데이트
794
809
  if (state.interventionEnabled === false) {
795
810
  logger.warn(`[${state.laneName}] Continue signal skipped (intervention disabled). Stall will escalate on next check.`);
@@ -803,16 +818,26 @@ export class StallDetectionService {
803
818
  return;
804
819
  }
805
820
 
806
- const interventionPath = safeJoin(state.laneRunDir, 'intervention.txt');
807
-
808
821
  try {
809
- fs.writeFileSync(interventionPath, 'continue');
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
+ }
810
835
 
811
836
  state.phase = StallPhase.CONTINUE_SENT;
812
837
  state.lastPhaseChangeTime = Date.now();
813
838
  state.continueSignalCount++;
814
839
 
815
- logger.info(`[${state.laneName}] Sent continue signal (#${state.continueSignalCount})`);
840
+ logger.info(`[${state.laneName}] Continue signal queued (#${state.continueSignalCount}) - agent will resume with intervention`);
816
841
 
817
842
  events.emit('recovery.continue_signal', {
818
843
  laneName: state.laneName,
@@ -825,9 +850,9 @@ export class StallDetectionService {
825
850
  }
826
851
 
827
852
  /**
828
- * Stronger prompt 발송
853
+ * Stronger prompt 발송 - 프로세스 중단 및 강력한 개입 메시지와 함께 resume
829
854
  */
830
- private sendStrongerPrompt(state: LaneStallState): void {
855
+ private async sendStrongerPrompt(state: LaneStallState): Promise<void> {
831
856
  // Intervention이 비활성화된 경우 신호를 보내지 않고 phase만 업데이트
832
857
  if (state.interventionEnabled === false) {
833
858
  logger.warn(`[${state.laneName}] Stronger prompt skipped (intervention disabled). Will escalate to restart.`);
@@ -841,20 +866,25 @@ export class StallDetectionService {
841
866
  return;
842
867
  }
843
868
 
844
- const interventionPath = safeJoin(state.laneRunDir, 'intervention.txt');
845
- const prompt = `[SYSTEM INTERVENTION] You seem to be stuck or waiting.
846
- Please continue with your current task immediately.
847
- If you're waiting for something, explain what you need and proceed with what you can do now.
848
- If you've completed the task, please summarize your work and finish.
849
- If you encountered a git error, resolve it and continue.`;
850
-
851
869
  try {
852
- fs.writeFileSync(interventionPath, prompt);
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
+ }
853
883
 
854
884
  state.phase = StallPhase.STRONGER_PROMPT_SENT;
855
885
  state.lastPhaseChangeTime = Date.now();
856
886
 
857
- logger.warn(`[${state.laneName}] Sent stronger prompt after continue signal failed`);
887
+ logger.warn(`[${state.laneName}] Stronger prompt queued - agent will resume with intervention`);
858
888
 
859
889
  events.emit('recovery.stronger_prompt', {
860
890
  laneName: state.laneName,
@@ -865,16 +895,31 @@ If you encountered a git error, resolve it and continue.`;
865
895
  }
866
896
 
867
897
  /**
868
- * 재시작 요청 (프로세스 종료)
898
+ * 재시작 요청 - 프로세스 종료 및 재시작 메시지와 함께 resume
869
899
  */
870
- private requestRestart(state: LaneStallState): void {
900
+ private async requestRestart(state: LaneStallState): Promise<void> {
871
901
  state.restartCount++;
872
902
  state.phase = StallPhase.RESTART_REQUESTED;
873
903
  state.lastPhaseChangeTime = Date.now();
874
904
 
875
- // 프로세스 종료
876
- if (state.childProcess && !state.childProcess.killed) {
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) {
877
921
  try {
922
+ // SIGKILL로 즉시 종료 (SIGTERM이 안 먹힐 수 있으므로)
878
923
  state.childProcess.kill('SIGKILL');
879
924
  logger.info(`[StallService] [${state.laneName}] Killed process ${state.childProcess.pid}`);
880
925
  } catch (error: any) {
@@ -882,7 +927,7 @@ If you encountered a git error, resolve it and continue.`;
882
927
  }
883
928
  }
884
929
 
885
- logger.warn(`[${state.laneName}] Killing and restarting lane (restart #${state.restartCount})`);
930
+ logger.warn(`[${state.laneName}] Restart requested (restart #${state.restartCount}/${this.config.maxRestarts})`);
886
931
 
887
932
  events.emit('recovery.restart', {
888
933
  laneName: state.laneName,
@@ -0,0 +1,256 @@
1
+ /**
2
+ * CursorFlow Hook System - Context Builders
3
+ *
4
+ * 각 Hook Point에 대한 컨텍스트를 생성하는 빌더 함수들입니다.
5
+ */
6
+
7
+ import {
8
+ HookContext,
9
+ BeforeTaskContext,
10
+ AfterTaskContext,
11
+ OnErrorContext,
12
+ OnStallContext,
13
+ OnLaneEndContext,
14
+ TaskDefinition,
15
+ TaskResult,
16
+ DependencyResult,
17
+ FlowController,
18
+ HookDataAccessor,
19
+ } from '../types';
20
+ import { createDataAccessor, DataAccessorOptions } from '../data-accessor';
21
+ import { createFlowController, FlowControllerOptions, FlowControllerImpl } from '../flow-controller';
22
+
23
+ // ============================================================================
24
+ // Base Context Builder Options
25
+ // ============================================================================
26
+
27
+ export interface BaseContextOptions {
28
+ /** Lane 이름 */
29
+ laneName: string;
30
+ /** Run ID */
31
+ runId: string;
32
+ /** 현재 태스크 인덱스 */
33
+ taskIndex: number;
34
+ /** 전체 태스크 수 */
35
+ totalTasks: number;
36
+ /** 현재 태스크 */
37
+ task: {
38
+ name: string;
39
+ prompt: string;
40
+ model: string;
41
+ dependsOn?: string[];
42
+ };
43
+ /** Worktree 디렉토리 */
44
+ worktreeDir: string;
45
+ /** Run 디렉토리 */
46
+ runDir: string;
47
+ /** 태스크 브랜치 */
48
+ taskBranch: string;
49
+ /** 파이프라인 브랜치 */
50
+ pipelineBranch: string;
51
+ /** 태스크 파일 경로 */
52
+ tasksFile: string;
53
+ /** Chat ID */
54
+ chatId: string;
55
+ /** 태스크 목록 (수정 가능 참조) */
56
+ tasks: TaskDefinition[];
57
+ /** 완료된 태스크 목록 */
58
+ completedTasks: TaskResult[];
59
+ /** 의존성 결과 */
60
+ dependencyResults: DependencyResult[];
61
+ /** 태스크 시작 시간 */
62
+ taskStartTime: number;
63
+ /** Lane 시작 시간 */
64
+ laneStartTime: number;
65
+ /** AgentSupervisor 인스턴스 */
66
+ agentSupervisor?: any;
67
+ /** Run Root */
68
+ runRoot?: string;
69
+ }
70
+
71
+ // ============================================================================
72
+ // Context Builders
73
+ // ============================================================================
74
+
75
+ /**
76
+ * 기본 컨텍스트 생성
77
+ */
78
+ function createBaseContext(options: BaseContextOptions): {
79
+ context: Omit<HookContext, 'flow' | 'getData'>;
80
+ flowController: FlowControllerImpl;
81
+ dataAccessor: HookDataAccessor;
82
+ } {
83
+ // FlowController 생성
84
+ const flowControllerOptions: FlowControllerOptions = {
85
+ laneName: options.laneName,
86
+ runDir: options.runDir,
87
+ worktreeDir: options.worktreeDir,
88
+ currentTaskIndex: options.taskIndex,
89
+ tasks: options.tasks,
90
+ tasksFile: options.tasksFile,
91
+ chatId: options.chatId,
92
+ agentSupervisor: options.agentSupervisor,
93
+ };
94
+
95
+ const flowController = createFlowController(flowControllerOptions);
96
+
97
+ // DataAccessor 생성
98
+ const dataAccessorOptions: DataAccessorOptions = {
99
+ worktreeDir: options.worktreeDir,
100
+ runDir: options.runDir,
101
+ taskBranch: options.taskBranch,
102
+ pipelineBranch: options.pipelineBranch,
103
+ laneName: options.laneName,
104
+ taskName: options.task.name,
105
+ completedTasks: options.completedTasks,
106
+ pendingTasks: options.tasks.slice(options.taskIndex + 1),
107
+ dependencyResults: options.dependencyResults,
108
+ taskStartTime: options.taskStartTime,
109
+ laneStartTime: options.laneStartTime,
110
+ runRoot: options.runRoot,
111
+ };
112
+
113
+ const dataAccessor = createDataAccessor(dataAccessorOptions);
114
+
115
+ // 기본 컨텍스트
116
+ const context = {
117
+ laneName: options.laneName,
118
+ runId: options.runId,
119
+ taskIndex: options.taskIndex,
120
+ totalTasks: options.totalTasks,
121
+ task: options.task,
122
+ };
123
+
124
+ return { context, flowController, dataAccessor };
125
+ }
126
+
127
+ /**
128
+ * beforeTask 컨텍스트 생성
129
+ */
130
+ export function createBeforeTaskContext(options: BaseContextOptions): {
131
+ context: BeforeTaskContext;
132
+ flowController: FlowControllerImpl;
133
+ } {
134
+ const { context, flowController, dataAccessor } = createBaseContext(options);
135
+
136
+ return {
137
+ context: {
138
+ ...context,
139
+ flow: flowController,
140
+ getData: dataAccessor,
141
+ },
142
+ flowController,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * afterTask 컨텍스트 생성
148
+ */
149
+ export function createAfterTaskContext(
150
+ options: BaseContextOptions,
151
+ result: {
152
+ status: 'success' | 'error' | 'blocked';
153
+ exitCode?: number;
154
+ error?: string;
155
+ }
156
+ ): {
157
+ context: AfterTaskContext;
158
+ flowController: FlowControllerImpl;
159
+ } {
160
+ const { context, flowController, dataAccessor } = createBaseContext(options);
161
+
162
+ return {
163
+ context: {
164
+ ...context,
165
+ flow: flowController,
166
+ getData: dataAccessor,
167
+ result,
168
+ },
169
+ flowController,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * onError 컨텍스트 생성
175
+ */
176
+ export function createOnErrorContext(
177
+ options: BaseContextOptions,
178
+ error: {
179
+ type: 'agent_error' | 'git_error' | 'timeout' | 'unknown';
180
+ message: string;
181
+ stack?: string;
182
+ retryable: boolean;
183
+ }
184
+ ): {
185
+ context: OnErrorContext;
186
+ flowController: FlowControllerImpl;
187
+ } {
188
+ const { context, flowController, dataAccessor } = createBaseContext(options);
189
+
190
+ return {
191
+ context: {
192
+ ...context,
193
+ flow: flowController,
194
+ getData: dataAccessor,
195
+ error,
196
+ },
197
+ flowController,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * onStall 컨텍스트 생성
203
+ */
204
+ export function createOnStallContext(
205
+ options: BaseContextOptions,
206
+ stall: {
207
+ idleTimeMs: number;
208
+ lastActivity: string;
209
+ bytesReceived: number;
210
+ phase: 'initial' | 'warning' | 'critical';
211
+ }
212
+ ): {
213
+ context: OnStallContext;
214
+ flowController: FlowControllerImpl;
215
+ } {
216
+ const { context, flowController, dataAccessor } = createBaseContext(options);
217
+
218
+ return {
219
+ context: {
220
+ ...context,
221
+ flow: flowController,
222
+ getData: dataAccessor,
223
+ stall,
224
+ },
225
+ flowController,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * onLaneEnd 컨텍스트 생성
231
+ */
232
+ export function createOnLaneEndContext(
233
+ options: BaseContextOptions,
234
+ summary: {
235
+ status: 'completed' | 'failed' | 'aborted';
236
+ completedTasks: number;
237
+ failedTasks: number;
238
+ totalDuration: number;
239
+ }
240
+ ): {
241
+ context: OnLaneEndContext;
242
+ flowController: FlowControllerImpl;
243
+ } {
244
+ const { context, flowController, dataAccessor } = createBaseContext(options);
245
+
246
+ return {
247
+ context: {
248
+ ...context,
249
+ flow: flowController,
250
+ getData: dataAccessor,
251
+ summary,
252
+ },
253
+ flowController,
254
+ };
255
+ }
256
+