@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.
Files changed (214) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/README.md +8 -3
  3. package/commands/cursorflow-init.md +0 -4
  4. package/dist/cli/index.js +0 -6
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/logs.js +108 -9
  7. package/dist/cli/logs.js.map +1 -1
  8. package/dist/cli/models.js +20 -3
  9. package/dist/cli/models.js.map +1 -1
  10. package/dist/cli/monitor.d.ts +7 -10
  11. package/dist/cli/monitor.js +1103 -1239
  12. package/dist/cli/monitor.js.map +1 -1
  13. package/dist/cli/resume.js +21 -1
  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 +99 -13
  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 +3 -117
  26. package/dist/core/auto-recovery.js +4 -482
  27. package/dist/core/auto-recovery.js.map +1 -1
  28. package/dist/core/failure-policy.d.ts +0 -53
  29. package/dist/core/failure-policy.js +7 -175
  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 +170 -0
  38. package/dist/core/intervention.js +408 -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 +39 -65
  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 +54 -36
  48. package/dist/core/runner/agent.js.map +1 -1
  49. package/dist/core/runner/pipeline.js +283 -123
  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 -80
  53. package/dist/core/runner/task.js.map +1 -1
  54. package/dist/core/runner.js +8 -2
  55. package/dist/core/runner.js.map +1 -1
  56. package/dist/core/stall-detection.d.ts +11 -4
  57. package/dist/core/stall-detection.js +64 -27
  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 +6 -1
  81. package/dist/services/logging/console.js.map +1 -1
  82. package/dist/services/logging/formatter.d.ts +9 -4
  83. package/dist/services/logging/formatter.js +64 -18
  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 +22 -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 +12 -1
  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 +10 -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 +98 -19
  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/log-constants.d.ts +1 -0
  129. package/dist/utils/log-constants.js +2 -1
  130. package/dist/utils/log-constants.js.map +1 -1
  131. package/dist/utils/log-formatter.d.ts +2 -1
  132. package/dist/utils/log-formatter.js +10 -10
  133. package/dist/utils/log-formatter.js.map +1 -1
  134. package/dist/utils/logger.d.ts +11 -0
  135. package/dist/utils/logger.js +82 -3
  136. package/dist/utils/logger.js.map +1 -1
  137. package/dist/utils/repro-thinking-logs.js +0 -13
  138. package/dist/utils/repro-thinking-logs.js.map +1 -1
  139. package/dist/utils/run-service.js +1 -1
  140. package/dist/utils/run-service.js.map +1 -1
  141. package/examples/README.md +0 -2
  142. package/examples/demo-project/README.md +1 -2
  143. package/package.json +13 -34
  144. package/scripts/setup-security.sh +0 -1
  145. package/scripts/test-log-parser.ts +171 -0
  146. package/scripts/verify-change.sh +272 -0
  147. package/src/cli/index.ts +0 -6
  148. package/src/cli/logs.ts +121 -10
  149. package/src/cli/models.ts +20 -3
  150. package/src/cli/monitor.ts +1273 -1342
  151. package/src/cli/resume.ts +27 -1
  152. package/src/cli/run.ts +29 -11
  153. package/src/cli/signal.ts +120 -18
  154. package/src/cli/tasks.ts +2 -59
  155. package/src/core/agent-supervisor.ts +64 -0
  156. package/src/core/auto-recovery.ts +14 -590
  157. package/src/core/failure-policy.ts +7 -229
  158. package/src/core/git-lifecycle-manager.ts +1011 -0
  159. package/src/core/git-pipeline-coordinator.ts +221 -0
  160. package/src/core/intervention.ts +463 -0
  161. package/src/core/lane-state-machine.ts +1097 -0
  162. package/src/core/orchestrator.ts +48 -64
  163. package/src/core/runner/agent.ts +77 -39
  164. package/src/core/runner/pipeline.ts +318 -138
  165. package/src/core/runner/task.ts +12 -97
  166. package/src/core/runner.ts +8 -2
  167. package/src/core/stall-detection.ts +74 -27
  168. package/src/hooks/contexts/index.ts +256 -0
  169. package/src/hooks/data-accessor.ts +488 -0
  170. package/src/hooks/flow-controller.ts +425 -0
  171. package/src/hooks/index.ts +154 -0
  172. package/src/hooks/manager.ts +434 -0
  173. package/src/hooks/types.ts +544 -0
  174. package/src/services/logging/buffer.ts +104 -43
  175. package/src/services/logging/console.ts +7 -1
  176. package/src/services/logging/formatter.ts +74 -18
  177. package/src/services/logging/index.ts +0 -2
  178. package/src/services/logging/paths.ts +14 -0
  179. package/src/services/logging/raw-log.ts +43 -0
  180. package/src/services/process/index.ts +1 -1
  181. package/src/types/agent.ts +15 -0
  182. package/src/types/config.ts +23 -1
  183. package/src/types/event-categories.ts +663 -0
  184. package/src/types/events.ts +0 -25
  185. package/src/types/flow.ts +10 -6
  186. package/src/types/index.ts +50 -4
  187. package/src/types/lane.ts +1 -2
  188. package/src/types/logging.ts +2 -1
  189. package/src/types/task.ts +12 -1
  190. package/src/ui/log-viewer.ts +3 -0
  191. package/src/utils/config.ts +11 -1
  192. package/src/utils/cursor-agent.ts +68 -16
  193. package/src/utils/enhanced-logger.ts +105 -19
  194. package/src/utils/event-registry.ts +595 -0
  195. package/src/utils/events.ts +0 -16
  196. package/src/utils/flow.ts +83 -0
  197. package/src/utils/log-constants.ts +2 -1
  198. package/src/utils/log-formatter.ts +10 -11
  199. package/src/utils/logger.ts +49 -3
  200. package/src/utils/repro-thinking-logs.ts +0 -15
  201. package/src/utils/run-service.ts +1 -1
  202. package/dist/cli/prepare.d.ts +0 -7
  203. package/dist/cli/prepare.js +0 -690
  204. package/dist/cli/prepare.js.map +0 -1
  205. package/dist/services/logging/file-writer.d.ts +0 -71
  206. package/dist/services/logging/file-writer.js +0 -516
  207. package/dist/services/logging/file-writer.js.map +0 -1
  208. package/dist/types/review.d.ts +0 -17
  209. package/dist/types/review.js +0 -6
  210. package/dist/types/review.js.map +0 -1
  211. package/scripts/ai-security-check.js +0 -233
  212. package/src/cli/prepare.ts +0 -777
  213. package/src/services/logging/file-writer.ts +0 -526
  214. 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,27 @@ 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
+ });
810
829
 
830
+ // 2. 상태 먼저 업데이트 (race condition 방지)
811
831
  state.phase = StallPhase.CONTINUE_SENT;
812
832
  state.lastPhaseChangeTime = Date.now();
813
833
  state.continueSignalCount++;
834
+
835
+ // 3. 프로세스 종료 (있는 경우)
836
+ if (state.childProcess?.pid && !state.childProcess.killed) {
837
+ logger.info(`[${state.laneName}] Interrupting process ${state.childProcess.pid} for continue signal`);
838
+ await killAndWait(state.childProcess.pid);
839
+ }
814
840
 
815
- logger.info(`[${state.laneName}] Sent continue signal (#${state.continueSignalCount})`);
841
+ logger.info(`[${state.laneName}] Continue signal queued (#${state.continueSignalCount}) - agent will resume with intervention`);
816
842
 
817
843
  events.emit('recovery.continue_signal', {
818
844
  laneName: state.laneName,
@@ -825,9 +851,9 @@ export class StallDetectionService {
825
851
  }
826
852
 
827
853
  /**
828
- * Stronger prompt 발송
854
+ * Stronger prompt 발송 - 프로세스 중단 및 강력한 개입 메시지와 함께 resume
829
855
  */
830
- private sendStrongerPrompt(state: LaneStallState): void {
856
+ private async sendStrongerPrompt(state: LaneStallState): Promise<void> {
831
857
  // Intervention이 비활성화된 경우 신호를 보내지 않고 phase만 업데이트
832
858
  if (state.interventionEnabled === false) {
833
859
  logger.warn(`[${state.laneName}] Stronger prompt skipped (intervention disabled). Will escalate to restart.`);
@@ -841,20 +867,26 @@ export class StallDetectionService {
841
867
  return;
842
868
  }
843
869
 
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
870
  try {
852
- fs.writeFileSync(interventionPath, prompt);
871
+ // 1. 개입 요청 생성
872
+ createInterventionRequest(state.laneRunDir, {
873
+ type: InterventionType.STRONGER_PROMPT,
874
+ message: createStrongerPromptMessage(),
875
+ source: 'stall-detector',
876
+ priority: 7,
877
+ });
853
878
 
879
+ // 2. 상태 먼저 업데이트 (race condition 방지)
854
880
  state.phase = StallPhase.STRONGER_PROMPT_SENT;
855
881
  state.lastPhaseChangeTime = Date.now();
882
+
883
+ // 3. 프로세스 종료 (있는 경우)
884
+ if (state.childProcess?.pid && !state.childProcess.killed) {
885
+ logger.warn(`[${state.laneName}] Interrupting process ${state.childProcess.pid} for stronger prompt`);
886
+ await killAndWait(state.childProcess.pid);
887
+ }
856
888
 
857
- logger.warn(`[${state.laneName}] Sent stronger prompt after continue signal failed`);
889
+ logger.warn(`[${state.laneName}] Stronger prompt queued - agent will resume with intervention`);
858
890
 
859
891
  events.emit('recovery.stronger_prompt', {
860
892
  laneName: state.laneName,
@@ -865,16 +897,31 @@ If you encountered a git error, resolve it and continue.`;
865
897
  }
866
898
 
867
899
  /**
868
- * 재시작 요청 (프로세스 종료)
900
+ * 재시작 요청 - 프로세스 종료 및 재시작 메시지와 함께 resume
869
901
  */
870
- private requestRestart(state: LaneStallState): void {
902
+ private async requestRestart(state: LaneStallState): Promise<void> {
871
903
  state.restartCount++;
872
904
  state.phase = StallPhase.RESTART_REQUESTED;
873
905
  state.lastPhaseChangeTime = Date.now();
874
906
 
875
- // 프로세스 종료
876
- if (state.childProcess && !state.childProcess.killed) {
907
+ // 1. 개입 요청 생성 (재시작 메시지)
908
+ if (state.laneRunDir) {
909
+ createInterventionRequest(state.laneRunDir, {
910
+ type: InterventionType.SYSTEM_RESTART,
911
+ message: createRestartMessage('Agent became unresponsive after multiple intervention attempts'),
912
+ source: 'stall-detector',
913
+ priority: 9,
914
+ metadata: {
915
+ restartCount: state.restartCount,
916
+ maxRestarts: this.config.maxRestarts,
917
+ },
918
+ });
919
+ }
920
+
921
+ // 2. 프로세스 종료 (SIGKILL 사용 - 강제 종료)
922
+ if (state.childProcess?.pid && !state.childProcess.killed) {
877
923
  try {
924
+ // SIGKILL로 즉시 종료 (SIGTERM이 안 먹힐 수 있으므로)
878
925
  state.childProcess.kill('SIGKILL');
879
926
  logger.info(`[StallService] [${state.laneName}] Killed process ${state.childProcess.pid}`);
880
927
  } catch (error: any) {
@@ -882,7 +929,7 @@ If you encountered a git error, resolve it and continue.`;
882
929
  }
883
930
  }
884
931
 
885
- logger.warn(`[${state.laneName}] Killing and restarting lane (restart #${state.restartCount})`);
932
+ logger.warn(`[${state.laneName}] Restart requested (restart #${state.restartCount}/${this.config.maxRestarts})`);
886
933
 
887
934
  events.emit('recovery.restart', {
888
935
  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
+