@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
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,20 +1,33 @@
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
+ createInterventionRequest,
23
+ InterventionType,
24
+ wrapUserIntervention,
25
+ } from '../core/intervention';
13
26
 
14
27
  interface SignalOptions {
15
28
  lane: string | null;
16
29
  message: string | null;
17
- timeout: number | null; // New timeout in milliseconds
30
+ timeout: number | null;
18
31
  runDir: string | null;
19
32
  help: boolean;
20
33
  }
@@ -24,14 +37,26 @@ function printHelp(): void {
24
37
  Usage: cursorflow signal <lane> "<message>" [options]
25
38
  cursorflow signal <lane> --timeout <ms>
26
39
 
27
- Directly intervene in a running lane.
40
+ Send an intervention message to a lane. For running lanes, the agent will be
41
+ interrupted immediately and resume with your message. For pending/waiting/failed
42
+ lanes, the message will be applied when the lane starts or resumes.
28
43
 
29
- Options:
44
+ Note: Completed lanes cannot receive signals.
45
+ To re-run a completed lane, start a new run with: cursorflow run
46
+
47
+ Arguments:
30
48
  <lane> Lane name to signal
31
49
  "<message>" Message text to send to the agent
50
+
51
+ Options:
32
52
  --timeout <ms> Update execution timeout (in milliseconds)
33
53
  --run-dir <path> Use a specific run directory (default: latest)
34
54
  --help, -h Show help
55
+
56
+ Examples:
57
+ cursorflow signal lane-1 "Please focus on error handling first"
58
+ cursorflow signal lane-2 "Skip the optional tasks and finish"
59
+ cursorflow signal lane-1 --timeout 600000 # Set 10 minute timeout
35
60
  `);
36
61
  }
37
62
 
@@ -39,8 +64,18 @@ function parseArgs(args: string[]): SignalOptions {
39
64
  const runDirIdx = args.indexOf('--run-dir');
40
65
  const timeoutIdx = args.indexOf('--timeout');
41
66
 
67
+ // Collect indices of option values to exclude from nonOptions
68
+ const optionValueIndices = new Set<number>();
69
+ if (runDirIdx >= 0 && runDirIdx + 1 < args.length) {
70
+ optionValueIndices.add(runDirIdx + 1);
71
+ }
72
+ if (timeoutIdx >= 0 && timeoutIdx + 1 < args.length) {
73
+ optionValueIndices.add(timeoutIdx + 1);
74
+ }
75
+
42
76
  // First non-option is lane, second (or rest joined) is message
43
- const nonOptions = args.filter(a => !a.startsWith('--'));
77
+ // Exclude option flags and their values
78
+ const nonOptions = args.filter((a, i) => !a.startsWith('--') && !optionValueIndices.has(i));
44
79
 
45
80
  return {
46
81
  lane: nonOptions[0] || null,
@@ -63,6 +98,39 @@ function findLatestRunDir(logsDir: string): string | null {
63
98
  return runs.length > 0 ? safeJoin(runsDir, runs[0]!) : null;
64
99
  }
65
100
 
101
+ /**
102
+ * Lane 상태 및 PID 확인
103
+ */
104
+ function getLaneStatus(laneDir: string): { state: LaneState | null; isRunning: boolean; pid?: number } {
105
+ const statePath = safeJoin(laneDir, 'state.json');
106
+
107
+ if (!fs.existsSync(statePath)) {
108
+ return { state: null, isRunning: false };
109
+ }
110
+
111
+ const state = loadState<LaneState>(statePath);
112
+ if (!state) {
113
+ return { state: null, isRunning: false };
114
+ }
115
+
116
+ const pid = state.pid;
117
+ const isRunning = pid ? isProcessAlive(pid) : false;
118
+
119
+ return { state, isRunning, pid };
120
+ }
121
+
122
+ /**
123
+ * 개입 요청 파일 작성 (비실행 중인 lane용)
124
+ */
125
+ function sendInterventionRequest(laneDir: string, message: string): void {
126
+ createInterventionRequest(laneDir, {
127
+ type: InterventionType.USER_MESSAGE,
128
+ message: wrapUserIntervention(message),
129
+ source: 'user',
130
+ priority: 10
131
+ });
132
+ }
133
+
66
134
  async function signal(args: string[]): Promise<void> {
67
135
  const options = parseArgs(args);
68
136
 
@@ -92,32 +160,67 @@ async function signal(args: string[]): Promise<void> {
92
160
  throw new Error(`Lane directory not found: ${laneDir}`);
93
161
  }
94
162
 
95
- // Case 1: Timeout update
163
+ // Case 1: Timeout update (기존 로직 유지)
96
164
  if (options.timeout !== null) {
97
165
  const timeoutPath = safeJoin(laneDir, 'timeout.txt');
98
166
  fs.writeFileSync(timeoutPath, String(options.timeout));
99
- logger.success(`Timeout update signal sent to ${options.lane}: ${options.timeout}ms`);
167
+ logger.success(`⏱ Timeout update signal sent to ${options.lane}: ${options.timeout}ms`);
100
168
  return;
101
169
  }
102
170
 
103
171
  // Case 2: Intervention message
104
172
  if (options.message) {
105
- const interventionPath = safeJoin(laneDir, 'intervention.txt');
173
+ const { state, isRunning, pid } = getLaneStatus(laneDir);
106
174
  const convoPath = safeJoin(laneDir, 'conversation.jsonl');
107
175
 
108
- logger.info(`Sending signal to lane: ${options.lane}`);
109
- logger.info(`Message: "${options.message}"`);
176
+ logger.info(`📨 Sending intervention to lane: ${options.lane}`);
177
+ logger.info(` Message: "${options.message.substring(0, 50)}${options.message.length > 50 ? '...' : ''}"`);
110
178
 
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
179
+ // Completed 레인은 signal 거부 (브랜치 충돌 방지)
180
+ if (state?.status === 'completed') {
181
+ logger.error(`❌ Cannot signal a completed lane.`);
182
+ logger.info(' To re-run this lane, start a new run with: cursorflow run');
183
+ throw new Error('Lane is already completed');
184
+ }
185
+
186
+ // Log to conversation for history
115
187
  const entry = createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${options.message}`, {
116
188
  task: 'DIRECT_SIGNAL'
117
189
  });
118
190
  appendLog(convoPath, entry);
191
+
192
+ // Lane이 실행 중이 아닌 경우 (pending/waiting/failed/paused)
193
+ if (!isRunning) {
194
+ // 실행 중이 아니면 다음 시작/resume 시 적용되도록 파일만 작성
195
+ sendInterventionRequest(laneDir, options.message);
196
+ logger.info(`ℹ Lane ${options.lane} is not currently running (status: ${state?.status || 'unknown'}).`);
197
+ logger.success('✅ Signal queued. Message will be applied when lane starts or resumes.');
198
+ return;
199
+ }
200
+
201
+ // 즉각 개입 실행: 프로세스 종료 + pending-intervention.json 생성
202
+ logger.info(`🛑 Interrupting running process (PID: ${pid})...`);
203
+
204
+ const result: InterventionResult = await executeUserIntervention(laneDir, options.message, pid);
205
+
206
+ if (result.success) {
207
+ if (result.killedPid) {
208
+ logger.success(`✅ Process ${result.killedPid} interrupted successfully.`);
209
+ logger.info(' The agent will resume with your intervention message.');
210
+ logger.info(' Monitor progress with: cursorflow monitor');
211
+ } else {
212
+ logger.success('✅ Intervention request created.');
213
+ logger.info(' Message will be applied on next agent turn.');
214
+ }
215
+ } else {
216
+ logger.error(`❌ Failed to send intervention: ${result.error}`);
217
+
218
+ // 실패해도 파일은 작성되었으므로 다음 기회에 적용됨
219
+ if (result.pendingFile) {
220
+ logger.info(' Intervention file was created and will be applied on next opportunity.');
221
+ }
222
+ }
119
223
 
120
- logger.success('Signal sent successfully. The agent will see this message in its current turn or next step.');
121
224
  return;
122
225
  }
123
226
 
@@ -125,4 +228,3 @@ async function signal(args: string[]): Promise<void> {
125
228
  }
126
229
 
127
230
  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
+ }