@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
package/src/cli/resume.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  ParsedMessage
19
19
  } from '../utils/enhanced-logger';
20
20
  import { formatMessageForConsole } from '../utils/log-formatter';
21
+ import { MAIN_LOG_FILENAME } from '../utils/log-constants';
21
22
 
22
23
  interface ResumeOptions {
23
24
  lane: string | null;
@@ -30,6 +31,7 @@ interface ResumeOptions {
30
31
  maxConcurrent: number;
31
32
  help: boolean;
32
33
  executor: string | null;
34
+ browser: boolean;
33
35
  }
34
36
 
35
37
  function printHelp(): void {
@@ -48,6 +50,7 @@ Options:
48
50
  --restart Restart from the first task (index 0)
49
51
  --skip-doctor Skip environment/branch checks (not recommended)
50
52
  --executor <type> Override executor (default: cursor-agent)
53
+ --browser Enable browser automation for all resumed tasks
51
54
  --help, -h Show help
52
55
 
53
56
  Examples:
@@ -56,6 +59,7 @@ Examples:
56
59
  cursorflow resume lane-1 # Resume single lane
57
60
  cursorflow resume _cursorflow/flows/MyFeature # Resume all lanes in flow
58
61
  cursorflow resume --all --restart # Restart all incomplete lanes from task 0
62
+ cursorflow resume --all --browser # Resume all lanes with browser mode enabled
59
63
  `);
60
64
  }
61
65
 
@@ -75,6 +79,7 @@ function parseArgs(args: string[]): ResumeOptions {
75
79
  maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '3') : 3,
76
80
  help: args.includes('--help') || args.includes('-h'),
77
81
  executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
82
+ browser: args.includes('--browser'),
78
83
  };
79
84
  }
80
85
 
@@ -103,7 +108,6 @@ const STATUS_COLORS: Record<string, string> = {
103
108
  failed: '\x1b[31m', // red
104
109
  paused: '\x1b[35m', // magenta
105
110
  waiting: '\x1b[33m', // yellow
106
- reviewing: '\x1b[36m', // cyan
107
111
  unknown: '\x1b[90m', // gray
108
112
  };
109
113
  const RESET = '\x1b[0m';
@@ -396,6 +400,7 @@ function spawnLaneResume(
396
400
  restart: boolean;
397
401
  pipelineBranch?: string;
398
402
  executor?: string | null;
403
+ browser?: boolean;
399
404
  enhancedLogConfig?: any;
400
405
  laneIndex?: number;
401
406
  }
@@ -425,6 +430,11 @@ function spawnLaneResume(
425
430
  runnerArgs.push('--executor', options.executor);
426
431
  }
427
432
 
433
+ // Pass browser flag if provided
434
+ if (options.browser) {
435
+ runnerArgs.push('--browser');
436
+ }
437
+
428
438
  // Generate lane label: [laneIdx-taskIdx-laneName] format, padded to 18 chars
429
439
  // Note: taskName will be updated dynamically via logManager.setTask()
430
440
  let currentTaskName = '';
@@ -493,6 +503,7 @@ async function resumeLanes(
493
503
  maxConcurrent: number;
494
504
  skipDoctor: boolean;
495
505
  executor: string | null;
506
+ browser: boolean;
496
507
  enhancedLogConfig?: any;
497
508
  }
498
509
  ): Promise<{ succeeded: string[]; failed: string[]; skipped: string[] }> {
@@ -587,10 +598,21 @@ async function resumeLanes(
587
598
  const { child } = spawnLaneResume(lane.name, lane.dir, lane.state!, {
588
599
  restart: options.restart,
589
600
  executor: options.executor,
601
+ browser: options.browser,
590
602
  enhancedLogConfig: options.enhancedLogConfig,
591
603
  laneIndex: laneIndexMap.get(lane.name) ?? 1,
592
604
  });
593
605
 
606
+ // Update state.json with the new PID for monitor tracking
607
+ const statePath = safeJoin(lane.dir, 'state.json');
608
+ const updatedState: LaneState = {
609
+ ...lane.state!,
610
+ status: 'running',
611
+ pid: child.pid,
612
+ error: null,
613
+ };
614
+ saveState(statePath, updatedState);
615
+
594
616
  active.set(lane.name, child);
595
617
 
596
618
  waitForChild(child).then(code => {
@@ -664,6 +686,9 @@ async function resume(args: string[]): Promise<void> {
664
686
  if (!runDir || !fs.existsSync(runDir)) {
665
687
  throw new Error(`Run directory not found: ${runDir || 'latest'}. Have you run any tasks yet?`);
666
688
  }
689
+
690
+ logger.setDefaultContext('MAIN');
691
+ logger.setLogFile(safeJoin(runDir, MAIN_LOG_FILENAME));
667
692
 
668
693
  const allLanes = getAllLaneStatuses(runDir);
669
694
  let lanesToResume: LaneInfo[] = [];
@@ -736,6 +761,7 @@ async function resume(args: string[]): Promise<void> {
736
761
  maxConcurrent: options.maxConcurrent,
737
762
  skipDoctor: options.skipDoctor,
738
763
  executor: options.executor,
764
+ browser: options.browser,
739
765
  enhancedLogConfig: config.enhancedLogging,
740
766
  });
741
767
 
package/src/cli/run.ts CHANGED
@@ -10,7 +10,7 @@ import { getLogsDir, loadConfig } from '../utils/config';
10
10
  import { runDoctor, getDoctorStatus } from '../utils/doctor';
11
11
  import { areCommandsInstalled, setupCommands } from './setup-commands';
12
12
  import { safeJoin } from '../utils/path';
13
- import { findFlowDir } from '../utils/flow';
13
+ import { findFlowDir, findLatestFlowOrTask } from '../utils/flow';
14
14
  import { loadState } from '../utils/state';
15
15
  import { LaneState } from '../types';
16
16
 
@@ -119,6 +119,7 @@ interface RunOptions {
119
119
  skipDoctor: boolean;
120
120
  skipPreflight: boolean;
121
121
  raw: boolean;
122
+ browser: boolean;
122
123
  help: boolean;
123
124
  }
124
125
 
@@ -138,6 +139,7 @@ Options:
138
139
  --skip-doctor Skip environment checks (not recommended)
139
140
  --skip-preflight Skip preflight checks (Git remote, etc.)
140
141
  --raw Save raw logs (absolute raw, no processing)
142
+ --browser Enable browser automation for all tasks
141
143
  --dry-run Show execution plan without starting agents
142
144
  --help, -h Show help
143
145
 
@@ -160,6 +162,7 @@ function parseArgs(args: string[]): RunOptions {
160
162
  skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
161
163
  skipPreflight: args.includes('--skip-preflight'),
162
164
  raw: args.includes('--raw'),
165
+ browser: args.includes('--browser'),
163
166
  help: args.includes('--help') || args.includes('-h'),
164
167
  };
165
168
  }
@@ -182,11 +185,6 @@ async function run(args: string[]): Promise<void> {
182
185
  }
183
186
  }
184
187
 
185
- if (!options.tasksDir) {
186
- console.log('\nUsage: cursorflow run <tasks-dir> [options]');
187
- throw new Error('Tasks directory required');
188
- }
189
-
190
188
  const config = loadConfig();
191
189
  const logsDir = getLogsDir(config);
192
190
  const originalCwd = process.cwd();
@@ -198,11 +196,22 @@ async function run(args: string[]): Promise<void> {
198
196
  }
199
197
 
200
198
  // Resolve tasks dir:
201
- // 1. Prefer the exact path if it exists relative to original cwd
202
- // 2. Search in flowsDir by name
203
- // 3. Fall back to projectRoot-relative tasksDir for backward compatibility
199
+ // 1. If not provided, use the most recent flow or legacy task
200
+ // 2. Prefer the exact path if it exists relative to original cwd
201
+ // 3. Search in flowsDir by name
202
+ // 4. Fall back to projectRoot-relative tasksDir for backward compatibility
204
203
  let tasksDir = '';
205
- if (path.isAbsolute(options.tasksDir)) {
204
+
205
+ if (!options.tasksDir) {
206
+ const latestPath = findLatestFlowOrTask(config);
207
+ if (latestPath) {
208
+ logger.info(`No flow specified. Using the latest: ${path.basename(latestPath)}`);
209
+ tasksDir = latestPath;
210
+ } else {
211
+ console.log('\nUsage: cursorflow run <tasks-dir> [options]');
212
+ throw new Error('Tasks directory required (none found in flows or tasks directories)');
213
+ }
214
+ } else if (path.isAbsolute(options.tasksDir)) {
206
215
  tasksDir = options.tasksDir;
207
216
  } else {
208
217
  const relPath = path.resolve(originalCwd, options.tasksDir);
@@ -216,7 +225,15 @@ async function run(args: string[]): Promise<void> {
216
225
  tasksDir = foundFlow;
217
226
  } else {
218
227
  // Fallback to legacy tasksDir
219
- tasksDir = safeJoin(config.projectRoot, options.tasksDir);
228
+ const legacyTasksDir = safeJoin(config.projectRoot, config.tasksDir);
229
+ const foundLegacy = findFlowDir(legacyTasksDir, options.tasksDir);
230
+
231
+ if (foundLegacy) {
232
+ tasksDir = foundLegacy;
233
+ } else {
234
+ // Fallback to joining with project root
235
+ tasksDir = safeJoin(config.projectRoot, options.tasksDir);
236
+ }
220
237
  }
221
238
  }
222
239
  }
@@ -318,6 +335,7 @@ async function run(args: string[]): Promise<void> {
318
335
  ...(options.raw ? { raw: true } : {}),
319
336
  },
320
337
  skipPreflight: options.skipPreflight,
338
+ browser: options.browser,
321
339
  });
322
340
  } catch (error: any) {
323
341
  // Re-throw to be handled by the main entry point
package/src/cli/signal.ts CHANGED
@@ -1,21 +1,32 @@
1
1
  /**
2
2
  * CursorFlow signal command
3
3
  *
4
- * Send a direct message to a running lane
4
+ * 실행 중인 lane에 즉각적인 개입 메시지 전송
5
+ *
6
+ * 동작 방식:
7
+ * - 현재 실행 중인 cursor-agent 프로세스를 중단 (SIGTERM)
8
+ * - 개입 메시지를 pending-intervention.json에 저장
9
+ * - Orchestrator가 프로세스 종료를 감지하고 개입 메시지와 함께 resume
5
10
  */
6
11
 
7
- import * as path from 'path';
8
12
  import * as fs from 'fs';
9
13
  import * as logger from '../utils/logger';
10
14
  import { loadConfig, getLogsDir } from '../utils/config';
11
- import { appendLog, createConversationEntry } from '../utils/state';
15
+ import { appendLog, createConversationEntry, loadState } from '../utils/state';
12
16
  import { safeJoin } from '../utils/path';
17
+ import { LaneState } from '../types';
18
+ import {
19
+ executeUserIntervention,
20
+ isProcessAlive,
21
+ InterventionResult,
22
+ } from '../core/intervention';
13
23
 
14
24
  interface SignalOptions {
15
25
  lane: string | null;
16
26
  message: string | null;
17
- timeout: number | null; // New timeout in milliseconds
27
+ timeout: number | null;
18
28
  runDir: string | null;
29
+ force: boolean; // 프로세스 종료 없이 대기 모드로 전송
19
30
  help: boolean;
20
31
  }
21
32
 
@@ -24,14 +35,25 @@ function printHelp(): void {
24
35
  Usage: cursorflow signal <lane> "<message>" [options]
25
36
  cursorflow signal <lane> --timeout <ms>
26
37
 
27
- Directly intervene in a running lane.
38
+ Directly intervene in a running lane. The agent will be interrupted immediately
39
+ and resume with your intervention message.
28
40
 
29
- Options:
41
+ Arguments:
30
42
  <lane> Lane name to signal
31
43
  "<message>" Message text to send to the agent
44
+
45
+ Options:
32
46
  --timeout <ms> Update execution timeout (in milliseconds)
33
47
  --run-dir <path> Use a specific run directory (default: latest)
48
+ --force Send signal without interrupting current process
49
+ (message will be picked up on next task)
34
50
  --help, -h Show help
51
+
52
+ Examples:
53
+ cursorflow signal lane-1 "Please focus on error handling first"
54
+ cursorflow signal lane-2 "Skip the optional tasks and finish"
55
+ cursorflow signal lane-1 --timeout 600000 # Set 10 minute timeout
56
+ cursorflow signal lane-1 "Continue" --force # Don't interrupt, wait for next turn
35
57
  `);
36
58
  }
37
59
 
@@ -47,6 +69,7 @@ function parseArgs(args: string[]): SignalOptions {
47
69
  message: nonOptions.slice(1).join(' ') || null,
48
70
  timeout: timeoutIdx >= 0 ? parseInt(args[timeoutIdx + 1] || '0') || null : null,
49
71
  runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
72
+ force: args.includes('--force'),
50
73
  help: args.includes('--help') || args.includes('-h'),
51
74
  };
52
75
  }
@@ -63,6 +86,42 @@ function findLatestRunDir(logsDir: string): string | null {
63
86
  return runs.length > 0 ? safeJoin(runsDir, runs[0]!) : null;
64
87
  }
65
88
 
89
+ /**
90
+ * Lane 상태 및 PID 확인
91
+ */
92
+ function getLaneStatus(laneDir: string): { state: LaneState | null; isRunning: boolean; pid?: number } {
93
+ const statePath = safeJoin(laneDir, 'state.json');
94
+
95
+ if (!fs.existsSync(statePath)) {
96
+ return { state: null, isRunning: false };
97
+ }
98
+
99
+ const state = loadState<LaneState>(statePath);
100
+ if (!state) {
101
+ return { state: null, isRunning: false };
102
+ }
103
+
104
+ const pid = state.pid;
105
+ const isRunning = pid ? isProcessAlive(pid) : false;
106
+
107
+ return { state, isRunning, pid };
108
+ }
109
+
110
+ /**
111
+ * 기존 방식으로 intervention.txt만 작성 (--force 옵션용)
112
+ */
113
+ function sendLegacyIntervention(laneDir: string, message: string): void {
114
+ const interventionPath = safeJoin(laneDir, 'intervention.txt');
115
+ const convoPath = safeJoin(laneDir, 'conversation.jsonl');
116
+
117
+ fs.writeFileSync(interventionPath, message);
118
+
119
+ const entry = createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${message}`, {
120
+ task: 'DIRECT_SIGNAL'
121
+ });
122
+ appendLog(convoPath, entry);
123
+ }
124
+
66
125
  async function signal(args: string[]): Promise<void> {
67
126
  const options = parseArgs(args);
68
127
 
@@ -92,32 +151,72 @@ async function signal(args: string[]): Promise<void> {
92
151
  throw new Error(`Lane directory not found: ${laneDir}`);
93
152
  }
94
153
 
95
- // Case 1: Timeout update
154
+ // Case 1: Timeout update (기존 로직 유지)
96
155
  if (options.timeout !== null) {
97
156
  const timeoutPath = safeJoin(laneDir, 'timeout.txt');
98
157
  fs.writeFileSync(timeoutPath, String(options.timeout));
99
- logger.success(`Timeout update signal sent to ${options.lane}: ${options.timeout}ms`);
158
+ logger.success(`⏱ Timeout update signal sent to ${options.lane}: ${options.timeout}ms`);
100
159
  return;
101
160
  }
102
161
 
103
162
  // Case 2: Intervention message
104
163
  if (options.message) {
105
- const interventionPath = safeJoin(laneDir, 'intervention.txt');
164
+ const { state, isRunning, pid } = getLaneStatus(laneDir);
106
165
  const convoPath = safeJoin(laneDir, 'conversation.jsonl');
107
166
 
108
- logger.info(`Sending signal to lane: ${options.lane}`);
109
- logger.info(`Message: "${options.message}"`);
167
+ logger.info(`📨 Sending intervention to lane: ${options.lane}`);
168
+ logger.info(` Message: "${options.message.substring(0, 50)}${options.message.length > 50 ? '...' : ''}"`);
110
169
 
111
- // 1. Write to intervention.txt for live agents to pick up immediately via stdin
112
- fs.writeFileSync(interventionPath, options.message);
113
-
114
- // 2. Also append to conversation log for visibility and history
170
+ // Log to conversation for history
115
171
  const entry = createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${options.message}`, {
116
172
  task: 'DIRECT_SIGNAL'
117
173
  });
118
174
  appendLog(convoPath, entry);
175
+
176
+ // --force: 기존 방식 (프로세스 중단 없이 파일만 작성)
177
+ if (options.force) {
178
+ sendLegacyIntervention(laneDir, options.message);
179
+ logger.success('✅ Signal queued (--force mode). Message will be applied on next task.');
180
+ return;
181
+ }
182
+
183
+ // Lane이 실행 중이 아닌 경우
184
+ if (!isRunning) {
185
+ if (state?.status === 'completed') {
186
+ logger.warn(`⚠ Lane ${options.lane} is already completed.`);
187
+ return;
188
+ }
189
+
190
+ // 실행 중이 아니면 다음 resume 시 적용되도록 파일만 작성
191
+ sendLegacyIntervention(laneDir, options.message);
192
+ logger.info(`ℹ Lane ${options.lane} is not currently running (status: ${state?.status || 'unknown'}).`);
193
+ logger.success('✅ Signal queued. Message will be applied when lane resumes.');
194
+ return;
195
+ }
196
+
197
+ // 즉각 개입 실행: 프로세스 종료 + pending-intervention.json 생성
198
+ logger.info(`🛑 Interrupting running process (PID: ${pid})...`);
199
+
200
+ const result: InterventionResult = await executeUserIntervention(laneDir, options.message, pid);
201
+
202
+ if (result.success) {
203
+ if (result.killedPid) {
204
+ logger.success(`✅ Process ${result.killedPid} interrupted successfully.`);
205
+ logger.info(' The agent will resume with your intervention message.');
206
+ logger.info(' Monitor progress with: cursorflow monitor');
207
+ } else {
208
+ logger.success('✅ Intervention request created.');
209
+ logger.info(' Message will be applied on next agent turn.');
210
+ }
211
+ } else {
212
+ logger.error(`❌ Failed to send intervention: ${result.error}`);
213
+
214
+ // 실패해도 파일은 작성되었으므로 다음 기회에 적용됨
215
+ if (result.pendingFile) {
216
+ logger.info(' Intervention file was created and will be applied on next opportunity.');
217
+ }
218
+ }
119
219
 
120
- logger.success('Signal sent successfully. The agent will see this message in its current turn or next step.');
121
220
  return;
122
221
  }
123
222
 
@@ -125,4 +224,3 @@ async function signal(args: string[]): Promise<void> {
125
224
  }
126
225
 
127
226
  export = signal;
128
-
package/src/cli/tasks.ts CHANGED
@@ -15,18 +15,11 @@ import * as logger from '../utils/logger';
15
15
  import { TaskService, TaskDirInfo, ValidationStatus } from '../utils/task-service';
16
16
  import { findProjectRoot, loadConfig, getTasksDir, getFlowsDir } from '../utils/config';
17
17
  import { safeJoin } from '../utils/path';
18
+ import { listFlows } from '../utils/flow';
19
+ import { FlowInfo } from '../types/flow';
18
20
 
19
21
  const COLORS = logger.COLORS;
20
22
 
21
- interface FlowInfo {
22
- id: string;
23
- name: string;
24
- path: string;
25
- timestamp: Date;
26
- lanes: string[];
27
- status: string;
28
- }
29
-
30
23
  interface TasksCliOptions {
31
24
  validate: boolean;
32
25
  taskName: string | null;
@@ -75,56 +68,6 @@ function parseArgs(args: string[]): TasksCliOptions {
75
68
  return options;
76
69
  }
77
70
 
78
- /**
79
- * List flows from _cursorflow/flows/
80
- */
81
- function listFlows(flowsDir: string): FlowInfo[] {
82
- if (!fs.existsSync(flowsDir)) {
83
- return [];
84
- }
85
-
86
- const dirs = fs.readdirSync(flowsDir)
87
- .filter(name => {
88
- const dirPath = safeJoin(flowsDir, name);
89
- return fs.statSync(dirPath).isDirectory() && !name.startsWith('.');
90
- })
91
- .sort((a, b) => b.localeCompare(a)); // Most recent first
92
-
93
- return dirs.map(name => {
94
- const flowPath = safeJoin(flowsDir, name);
95
- const metaPath = safeJoin(flowPath, 'flow.meta.json');
96
-
97
- let meta: any = {};
98
- try {
99
- if (fs.existsSync(metaPath)) {
100
- meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
101
- }
102
- } catch {}
103
-
104
- // Parse flow name from directory (e.g., "001_TestFeature" -> "TestFeature")
105
- const match = name.match(/^(\d+)_(.+)$/);
106
- const id = match ? match[1] : name;
107
- const flowName = match ? match[2] : name;
108
-
109
- // Get lane files
110
- const laneFiles = fs.readdirSync(flowPath)
111
- .filter(f => f.endsWith('.json') && f !== 'flow.meta.json')
112
- .map(f => {
113
- const laneMatch = f.match(/^\d+-([^.]+)\.json$/);
114
- return laneMatch ? laneMatch[1] : f.replace('.json', '');
115
- });
116
-
117
- return {
118
- id,
119
- name: flowName,
120
- path: flowPath,
121
- timestamp: meta.createdAt ? new Date(meta.createdAt) : new Date(),
122
- lanes: laneFiles,
123
- status: meta.status || 'pending',
124
- };
125
- });
126
- }
127
-
128
71
  /**
129
72
  * Get flow info by name
130
73
  */
@@ -0,0 +1,64 @@
1
+ import { cursorAgentCreateChat, cursorAgentSend } from './runner/agent';
2
+ import { events } from '../utils/events';
3
+ import { AgentSendResult } from '../types';
4
+ import { analyzeFailure, logFailure } from './failure-policy';
5
+
6
+ export interface AgentPromptOptions {
7
+ workspaceDir: string;
8
+ chatId: string;
9
+ prompt: string;
10
+ model?: string;
11
+ laneName?: string;
12
+ signalDir?: string;
13
+ timeout?: number;
14
+ enableIntervention?: boolean;
15
+ outputFormat?: 'json' | 'stream-json';
16
+ taskName?: string;
17
+ /** Enable browser automation (--browser flag). Required for web testing/scraping. */
18
+ browser?: boolean;
19
+ /** Auto-approve commands (--force flag). Default: true */
20
+ autoApproveCommands?: boolean;
21
+ /** Auto-approve MCP servers (--approve-mcps flag). Default: true */
22
+ autoApproveMcps?: boolean;
23
+ }
24
+
25
+ export class AgentSupervisor {
26
+ createChat(workspaceDir?: string): string {
27
+ return cursorAgentCreateChat(workspaceDir);
28
+ }
29
+
30
+ async sendTaskPrompt(options: AgentPromptOptions): Promise<AgentSendResult> {
31
+ const startTime = Date.now();
32
+
33
+ events.emit('agent.prompt_sent', {
34
+ taskName: options.taskName || 'task',
35
+ model: options.model || 'unknown',
36
+ promptLength: options.prompt.length,
37
+ });
38
+
39
+ const result = await cursorAgentSend(options);
40
+ const duration = Date.now() - startTime;
41
+
42
+ events.emit('agent.response_received', {
43
+ taskName: options.taskName || 'task',
44
+ ok: result.ok,
45
+ duration,
46
+ responseLength: result.resultText?.length || 0,
47
+ error: result.error,
48
+ });
49
+
50
+ if (!result.ok) {
51
+ const analysis = analyzeFailure(result.error, {
52
+ circuitBreakerName: options.laneName,
53
+ });
54
+ logFailure(options.laneName || options.taskName || 'agent', analysis);
55
+ events.emit('agent.recovery_suggested', {
56
+ laneName: options.laneName,
57
+ taskName: options.taskName || 'task',
58
+ analysis,
59
+ });
60
+ }
61
+
62
+ return result;
63
+ }
64
+ }
@@ -84,6 +84,7 @@ export const DEFAULT_AUTO_RECOVERY_CONFIG: AutoRecoveryConfig = {
84
84
  /** State tracking for a single lane's recovery */
85
85
  export interface LaneRecoveryState {
86
86
  laneName: string;
87
+ runId: string;
87
88
  stage: RecoveryStage;
88
89
  lastActivityTime: number;
89
90
  lastBytesReceived: number;
@@ -242,10 +243,11 @@ export class AutoRecoveryManager {
242
243
  /**
243
244
  * Register a lane for recovery monitoring
244
245
  */
245
- registerLane(laneName: string): void {
246
+ registerLane(laneName: string, runId: string): void {
246
247
  const now = Date.now();
247
248
  this.laneStates.set(laneName, {
248
249
  laneName,
250
+ runId,
249
251
  stage: RecoveryStage.NORMAL,
250
252
  lastActivityTime: now,
251
253
  lastBytesReceived: 0,
@@ -461,6 +463,7 @@ export class AutoRecoveryManager {
461
463
  logger.warn(message);
462
464
 
463
465
  events.emit('recovery.continue_signal', {
466
+ runId: state.runId,
464
467
  laneName,
465
468
  idleSeconds,
466
469
  signalCount: state.continueSignalsSent,
@@ -521,6 +524,7 @@ If you encountered a git error, resolve it and continue.`;
521
524
  logger.warn(message);
522
525
 
523
526
  events.emit('recovery.stronger_prompt', {
527
+ runId: state.runId,
524
528
  laneName,
525
529
  prompt: strongerPrompt,
526
530
  });
@@ -580,6 +584,7 @@ If you encountered a git error, resolve it and continue.`;
580
584
  logger.warn(message);
581
585
 
582
586
  events.emit('recovery.restart', {
587
+ runId: state.runId,
583
588
  laneName,
584
589
  restartCount: state.restartCount,
585
590
  maxRestarts: this.config.maxRestarts,
@@ -676,6 +681,7 @@ If you encountered a git error, resolve it and continue.`;
676
681
  logger.error(message);
677
682
 
678
683
  events.emit('recovery.diagnosed', {
684
+ runId: state.runId,
679
685
  laneName,
680
686
  diagnostic,
681
687
  });
@@ -23,7 +23,6 @@ export enum FailureType {
23
23
  DEPENDENCY_BLOCK = 'DEPENDENCY_BLOCK',
24
24
  DEPENDENCY_FAILED = 'DEPENDENCY_FAILED',
25
25
  DEPENDENCY_TIMEOUT = 'DEPENDENCY_TIMEOUT',
26
- REVIEW_FAIL = 'REVIEW_FAIL',
27
26
  GIT_ERROR = 'GIT_ERROR',
28
27
  GIT_PUSH_REJECTED = 'GIT_PUSH_REJECTED',
29
28
  MERGE_CONFLICT = 'MERGE_CONFLICT',