@litmers/cursorflow-orchestrator 0.1.31 → 0.1.36

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 (150) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +182 -59
  3. package/commands/cursorflow-add.md +159 -0
  4. package/commands/cursorflow-doctor.md +45 -23
  5. package/commands/cursorflow-monitor.md +23 -2
  6. package/commands/cursorflow-new.md +87 -0
  7. package/commands/cursorflow-run.md +60 -111
  8. package/dist/cli/add.d.ts +7 -0
  9. package/dist/cli/add.js +377 -0
  10. package/dist/cli/add.js.map +1 -0
  11. package/dist/cli/clean.js +1 -0
  12. package/dist/cli/clean.js.map +1 -1
  13. package/dist/cli/config.d.ts +7 -0
  14. package/dist/cli/config.js +181 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/doctor.js +47 -4
  17. package/dist/cli/doctor.js.map +1 -1
  18. package/dist/cli/index.js +34 -30
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/logs.js +17 -34
  21. package/dist/cli/logs.js.map +1 -1
  22. package/dist/cli/monitor.js +62 -65
  23. package/dist/cli/monitor.js.map +1 -1
  24. package/dist/cli/new.d.ts +7 -0
  25. package/dist/cli/new.js +232 -0
  26. package/dist/cli/new.js.map +1 -0
  27. package/dist/cli/prepare.js +95 -193
  28. package/dist/cli/prepare.js.map +1 -1
  29. package/dist/cli/resume.js +57 -68
  30. package/dist/cli/resume.js.map +1 -1
  31. package/dist/cli/run.js +60 -30
  32. package/dist/cli/run.js.map +1 -1
  33. package/dist/cli/stop.js +6 -0
  34. package/dist/cli/stop.js.map +1 -1
  35. package/dist/cli/tasks.d.ts +5 -3
  36. package/dist/cli/tasks.js +181 -29
  37. package/dist/cli/tasks.js.map +1 -1
  38. package/dist/core/failure-policy.d.ts +9 -0
  39. package/dist/core/failure-policy.js +9 -0
  40. package/dist/core/failure-policy.js.map +1 -1
  41. package/dist/core/orchestrator.d.ts +20 -6
  42. package/dist/core/orchestrator.js +215 -334
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +27 -0
  45. package/dist/core/runner/agent.js +294 -0
  46. package/dist/core/runner/agent.js.map +1 -0
  47. package/dist/core/runner/index.d.ts +5 -0
  48. package/dist/core/runner/index.js +22 -0
  49. package/dist/core/runner/index.js.map +1 -0
  50. package/dist/core/runner/pipeline.d.ts +9 -0
  51. package/dist/core/runner/pipeline.js +539 -0
  52. package/dist/core/runner/pipeline.js.map +1 -0
  53. package/dist/core/runner/prompt.d.ts +25 -0
  54. package/dist/core/runner/prompt.js +175 -0
  55. package/dist/core/runner/prompt.js.map +1 -0
  56. package/dist/core/runner/task.d.ts +26 -0
  57. package/dist/core/runner/task.js +283 -0
  58. package/dist/core/runner/task.js.map +1 -0
  59. package/dist/core/runner/utils.d.ts +37 -0
  60. package/dist/core/runner/utils.js +161 -0
  61. package/dist/core/runner/utils.js.map +1 -0
  62. package/dist/core/runner.d.ts +2 -96
  63. package/dist/core/runner.js +11 -1136
  64. package/dist/core/runner.js.map +1 -1
  65. package/dist/core/stall-detection.d.ts +326 -0
  66. package/dist/core/stall-detection.js +781 -0
  67. package/dist/core/stall-detection.js.map +1 -0
  68. package/dist/services/logging/console.js +2 -1
  69. package/dist/services/logging/console.js.map +1 -1
  70. package/dist/types/config.d.ts +6 -6
  71. package/dist/types/flow.d.ts +84 -0
  72. package/dist/types/flow.js +10 -0
  73. package/dist/types/flow.js.map +1 -0
  74. package/dist/types/index.d.ts +1 -0
  75. package/dist/types/index.js +3 -3
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/types/lane.d.ts +0 -2
  78. package/dist/types/logging.d.ts +5 -1
  79. package/dist/types/task.d.ts +7 -11
  80. package/dist/utils/config.d.ts +5 -1
  81. package/dist/utils/config.js +15 -16
  82. package/dist/utils/config.js.map +1 -1
  83. package/dist/utils/dependency.d.ts +36 -1
  84. package/dist/utils/dependency.js +256 -1
  85. package/dist/utils/dependency.js.map +1 -1
  86. package/dist/utils/doctor.js +40 -8
  87. package/dist/utils/doctor.js.map +1 -1
  88. package/dist/utils/enhanced-logger.d.ts +45 -82
  89. package/dist/utils/enhanced-logger.js +239 -844
  90. package/dist/utils/enhanced-logger.js.map +1 -1
  91. package/dist/utils/flow.d.ts +9 -0
  92. package/dist/utils/flow.js +73 -0
  93. package/dist/utils/flow.js.map +1 -0
  94. package/dist/utils/git.d.ts +29 -0
  95. package/dist/utils/git.js +115 -5
  96. package/dist/utils/git.js.map +1 -1
  97. package/dist/utils/state.js +0 -2
  98. package/dist/utils/state.js.map +1 -1
  99. package/dist/utils/task-service.d.ts +2 -2
  100. package/dist/utils/task-service.js +40 -31
  101. package/dist/utils/task-service.js.map +1 -1
  102. package/package.json +4 -3
  103. package/src/cli/add.ts +397 -0
  104. package/src/cli/clean.ts +1 -0
  105. package/src/cli/config.ts +177 -0
  106. package/src/cli/doctor.ts +48 -4
  107. package/src/cli/index.ts +36 -32
  108. package/src/cli/logs.ts +20 -33
  109. package/src/cli/monitor.ts +70 -75
  110. package/src/cli/new.ts +235 -0
  111. package/src/cli/prepare.ts +98 -205
  112. package/src/cli/resume.ts +61 -76
  113. package/src/cli/run.ts +333 -306
  114. package/src/cli/stop.ts +8 -0
  115. package/src/cli/tasks.ts +200 -21
  116. package/src/core/failure-policy.ts +9 -0
  117. package/src/core/orchestrator.ts +279 -379
  118. package/src/core/runner/agent.ts +314 -0
  119. package/src/core/runner/index.ts +6 -0
  120. package/src/core/runner/pipeline.ts +567 -0
  121. package/src/core/runner/prompt.ts +174 -0
  122. package/src/core/runner/task.ts +320 -0
  123. package/src/core/runner/utils.ts +142 -0
  124. package/src/core/runner.ts +8 -1347
  125. package/src/core/stall-detection.ts +936 -0
  126. package/src/services/logging/console.ts +2 -1
  127. package/src/types/config.ts +6 -6
  128. package/src/types/flow.ts +91 -0
  129. package/src/types/index.ts +15 -3
  130. package/src/types/lane.ts +0 -2
  131. package/src/types/logging.ts +5 -1
  132. package/src/types/task.ts +7 -11
  133. package/src/utils/config.ts +16 -17
  134. package/src/utils/dependency.ts +311 -2
  135. package/src/utils/doctor.ts +36 -8
  136. package/src/utils/enhanced-logger.ts +264 -927
  137. package/src/utils/flow.ts +42 -0
  138. package/src/utils/git.ts +145 -5
  139. package/src/utils/state.ts +0 -2
  140. package/src/utils/task-service.ts +48 -40
  141. package/commands/cursorflow-review.md +0 -56
  142. package/commands/cursorflow-runs.md +0 -59
  143. package/dist/cli/runs.d.ts +0 -5
  144. package/dist/cli/runs.js +0 -214
  145. package/dist/cli/runs.js.map +0 -1
  146. package/dist/core/reviewer.d.ts +0 -66
  147. package/dist/core/reviewer.js +0 -265
  148. package/dist/core/reviewer.js.map +0 -1
  149. package/src/cli/runs.ts +0 -212
  150. package/src/core/reviewer.ts +0 -285
package/src/cli/run.ts CHANGED
@@ -1,306 +1,333 @@
1
- /**
2
- * CursorFlow run command
3
- */
4
-
5
- import * as path from 'path';
6
- import * as fs from 'fs';
7
- import * as logger from '../utils/logger';
8
- import { orchestrate } from '../core/orchestrator';
9
- import { getLogsDir, loadConfig } from '../utils/config';
10
- import { runDoctor, getDoctorStatus } from '../utils/doctor';
11
- import { areCommandsInstalled, setupCommands } from './setup-commands';
12
- import { safeJoin } from '../utils/path';
13
- import { loadState } from '../utils/state';
14
- import { LaneState } from '../types';
15
-
16
- interface IncompleteLaneInfo {
17
- name: string;
18
- status: string;
19
- taskIndex: number;
20
- totalTasks: number;
21
- error?: string;
22
- }
23
-
24
- interface ExistingRunInfo {
25
- runDir: string;
26
- runId: string;
27
- incompleteLanes: IncompleteLaneInfo[];
28
- completedLanes: string[];
29
- totalLanes: number;
30
- }
31
-
32
- /**
33
- * Find existing run for a tasks directory
34
- */
35
- function findExistingRunForTasks(logsDir: string, tasksDir: string): ExistingRunInfo | null {
36
- const runsDir = safeJoin(logsDir, 'runs');
37
- if (!fs.existsSync(runsDir)) return null;
38
-
39
- const runs = fs.readdirSync(runsDir)
40
- .filter(d => d.startsWith('run-'))
41
- .sort()
42
- .reverse(); // Latest first
43
-
44
- for (const runId of runs) {
45
- const runDir = safeJoin(runsDir, runId);
46
- const lanesDir = safeJoin(runDir, 'lanes');
47
-
48
- if (!fs.existsSync(lanesDir)) continue;
49
-
50
- const laneDirs = fs.readdirSync(lanesDir)
51
- .filter(f => fs.statSync(safeJoin(lanesDir, f)).isDirectory());
52
-
53
- if (laneDirs.length === 0) continue;
54
-
55
- // Check if any lane belongs to this tasks directory
56
- let matchesTasksDir = false;
57
- const incompleteLanes: IncompleteLaneInfo[] = [];
58
- const completedLanes: string[] = [];
59
-
60
- for (const laneName of laneDirs) {
61
- const statePath = safeJoin(lanesDir, laneName, 'state.json');
62
- if (!fs.existsSync(statePath)) continue;
63
-
64
- const state = loadState<LaneState>(statePath);
65
- if (!state) continue;
66
-
67
- // Check if this lane's tasks file is in the target tasks directory
68
- if (state.tasksFile) {
69
- const taskFileDir = path.dirname(state.tasksFile);
70
- if (path.resolve(taskFileDir) === path.resolve(tasksDir)) {
71
- matchesTasksDir = true;
72
- }
73
- }
74
-
75
- // Check completion status
76
- if (state.status === 'completed') {
77
- completedLanes.push(laneName);
78
- } else {
79
- // Check if process is alive (zombie detection)
80
- let isZombie = false;
81
- if (state.status === 'running' && state.pid) {
82
- try {
83
- process.kill(state.pid, 0);
84
- } catch {
85
- isZombie = true;
86
- }
87
- }
88
-
89
- incompleteLanes.push({
90
- name: laneName,
91
- status: isZombie ? 'zombie' : state.status,
92
- taskIndex: state.currentTaskIndex,
93
- totalTasks: state.totalTasks,
94
- error: state.error || undefined,
95
- });
96
- }
97
- }
98
-
99
- if (matchesTasksDir && incompleteLanes.length > 0) {
100
- return {
101
- runDir,
102
- runId,
103
- incompleteLanes,
104
- completedLanes,
105
- totalLanes: laneDirs.length,
106
- };
107
- }
108
- }
109
-
110
- return null;
111
- }
112
-
113
- interface RunOptions {
114
- tasksDir?: string;
115
- dryRun: boolean;
116
- executor: string | null;
117
- maxConcurrent: number | null;
118
- skipDoctor: boolean;
119
- noGit: boolean;
120
- raw: boolean;
121
- help: boolean;
122
- }
123
-
124
- function printHelp(): void {
125
- console.log(`
126
- Usage: cursorflow run <tasks-dir> [options]
127
-
128
- Run task orchestration based on dependency graph.
129
-
130
- If an existing run with incomplete lanes is found for the same tasks directory,
131
- it will automatically resume instead of starting a new run.
132
-
133
- Options:
134
- <tasks-dir> Directory containing task JSON files
135
- --max-concurrent <num> Limit parallel agents (overrides config)
136
- --executor <type> cursor-agent | cloud
137
- --skip-doctor Skip environment checks (not recommended)
138
- --no-git Disable Git operations (worktree, push, commit)
139
- --raw Save raw logs (absolute raw, no processing)
140
- --dry-run Show execution plan without starting agents
141
- --help, -h Show help
142
-
143
- Examples:
144
- cursorflow run _cursorflow/tasks
145
- cursorflow run _cursorflow/tasks --no-git --skip-doctor
146
- `);
147
- }
148
-
149
- function parseArgs(args: string[]): RunOptions {
150
- const tasksDir = args.find(a => !a.startsWith('--'));
151
- const executorIdx = args.indexOf('--executor');
152
- const maxConcurrentIdx = args.indexOf('--max-concurrent');
153
-
154
- return {
155
- tasksDir,
156
- dryRun: args.includes('--dry-run'),
157
- executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
158
- maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '0') || null : null,
159
- skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
160
- noGit: args.includes('--no-git'),
161
- raw: args.includes('--raw'),
162
- help: args.includes('--help') || args.includes('-h'),
163
- };
164
- }
165
-
166
- async function run(args: string[]): Promise<void> {
167
- const options = parseArgs(args);
168
-
169
- if (options.help) {
170
- printHelp();
171
- return;
172
- }
173
-
174
- // Auto-setup Cursor commands if missing or outdated
175
- if (!areCommandsInstalled()) {
176
- logger.info('Installing missing or outdated Cursor IDE commands...');
177
- try {
178
- setupCommands({ silent: true });
179
- } catch (e) {
180
- // Non-blocking
181
- }
182
- }
183
-
184
- if (!options.tasksDir) {
185
- console.log('\nUsage: cursorflow run <tasks-dir> [options]');
186
- throw new Error('Tasks directory required');
187
- }
188
-
189
- const config = loadConfig();
190
- const logsDir = getLogsDir(config);
191
-
192
- // Resolve tasks dir:
193
- // - Prefer the exact path if it exists relative to cwd
194
- // - Otherwise, fall back to projectRoot-relative path for better ergonomics
195
- const tasksDir =
196
- path.isAbsolute(options.tasksDir)
197
- ? options.tasksDir
198
- : (fs.existsSync(options.tasksDir)
199
- ? path.resolve(process.cwd(), options.tasksDir) // nosemgrep
200
- : safeJoin(config.projectRoot, options.tasksDir));
201
-
202
- if (!fs.existsSync(tasksDir)) {
203
- throw new Error(`Tasks directory not found: ${tasksDir}`);
204
- }
205
-
206
- // Check for existing incomplete run and auto-resume
207
- const existingRun = findExistingRunForTasks(logsDir, tasksDir);
208
- if (existingRun && existingRun.incompleteLanes.length > 0) {
209
- logger.section('📋 Existing Run Detected');
210
- logger.info(`Run: ${existingRun.runId}`);
211
- logger.info(`Completed: ${existingRun.completedLanes.length}/${existingRun.totalLanes} lanes`);
212
-
213
- console.log('');
214
- logger.info('Incomplete lanes:');
215
- for (const lane of existingRun.incompleteLanes) {
216
- const statusEmoji = lane.status === 'failed' ? '❌' :
217
- lane.status === 'zombie' ? '🧟' :
218
- lane.status === 'running' ? '🔄' : '⏸';
219
- logger.info(` ${statusEmoji} ${lane.name}: ${lane.status} (${lane.taskIndex}/${lane.totalTasks})`);
220
- if (lane.error) {
221
- logger.warn(` └─ ${lane.error.substring(0, 60)}${lane.error.length > 60 ? '...' : ''}`);
222
- }
223
- }
224
-
225
- console.log('');
226
- logger.info('🔄 Auto-resuming from existing run...');
227
- console.log('');
228
-
229
- // Call the resume command with --all flag
230
- const resumeCmd = require('./resume');
231
- const resumeArgs = [
232
- '--all',
233
- '--run-dir', existingRun.runDir,
234
- ];
235
-
236
- if (options.skipDoctor) resumeArgs.push('--skip-doctor');
237
- if (options.noGit) resumeArgs.push('--no-git');
238
- if (options.executor) {
239
- resumeArgs.push('--executor', options.executor);
240
- }
241
- if (options.maxConcurrent) {
242
- resumeArgs.push('--max-concurrent', String(options.maxConcurrent));
243
- }
244
-
245
- await resumeCmd(resumeArgs);
246
- return;
247
- }
248
-
249
- // Check if doctor has been run at least once
250
- const doctorStatus = getDoctorStatus(config.projectRoot);
251
- if (!doctorStatus) {
252
- logger.warn('It looks like you haven\'t run `cursorflow doctor` yet.');
253
- logger.warn('Running doctor is highly recommended to catch environment issues early.');
254
- console.log(' Run: cursorflow doctor\n');
255
- }
256
-
257
- // Preflight checks (doctor)
258
- if (!options.skipDoctor) {
259
- const report = runDoctor({
260
- cwd: process.cwd(),
261
- tasksDir,
262
- executor: options.executor || config.executor,
263
- includeCursorAgentChecks: true,
264
- });
265
-
266
- if (!report.ok) {
267
- logger.section('🛑 Pre-flight check failed');
268
- for (const issue of report.issues) {
269
- const header = `${issue.title} (${issue.id})`;
270
- if (issue.severity === 'error') {
271
- logger.error(header, { emoji: '❌' });
272
- } else {
273
- logger.warn(header, { emoji: '⚠️' });
274
- }
275
- console.log(` ${issue.message}`);
276
- if (issue.details) console.log(` Details: ${issue.details}`);
277
- if (issue.fixes?.length) {
278
- console.log(' Fix:');
279
- for (const fix of issue.fixes) console.log(` - ${fix}`);
280
- }
281
- console.log('');
282
- }
283
- throw new Error('Pre-flight checks failed. Run `cursorflow doctor` for details.');
284
- }
285
- }
286
-
287
- try {
288
- await orchestrate(tasksDir, {
289
- executor: options.executor || config.executor,
290
- pollInterval: config.pollInterval * 1000,
291
- runDir: path.join(logsDir, 'runs', `run-${Date.now()}`),
292
- maxConcurrentLanes: options.maxConcurrent || config.maxConcurrentLanes,
293
- webhooks: config.webhooks || [],
294
- enhancedLogging: {
295
- ...config.enhancedLogging,
296
- ...(options.raw ? { raw: true } : {}),
297
- },
298
- noGit: options.noGit,
299
- });
300
- } catch (error: any) {
301
- // Re-throw to be handled by the main entry point
302
- throw new Error(`Orchestration failed: ${error.message}`);
303
- }
304
- }
305
-
306
- export = run;
1
+ /**
2
+ * CursorFlow run command
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import * as fs from 'fs';
7
+ import * as logger from '../utils/logger';
8
+ import { orchestrate } from '../core/orchestrator';
9
+ import { getLogsDir, loadConfig } from '../utils/config';
10
+ import { runDoctor, getDoctorStatus } from '../utils/doctor';
11
+ import { areCommandsInstalled, setupCommands } from './setup-commands';
12
+ import { safeJoin } from '../utils/path';
13
+ import { findFlowDir } from '../utils/flow';
14
+ import { loadState } from '../utils/state';
15
+ import { LaneState } from '../types';
16
+
17
+ interface IncompleteLaneInfo {
18
+ name: string;
19
+ status: string;
20
+ taskIndex: number;
21
+ totalTasks: number;
22
+ error?: string;
23
+ }
24
+
25
+ interface ExistingRunInfo {
26
+ runDir: string;
27
+ runId: string;
28
+ incompleteLanes: IncompleteLaneInfo[];
29
+ completedLanes: string[];
30
+ totalLanes: number;
31
+ }
32
+
33
+ /**
34
+ * Find existing run for a tasks directory
35
+ */
36
+ function findExistingRunForTasks(logsDir: string, tasksDir: string): ExistingRunInfo | null {
37
+ const runsDir = safeJoin(logsDir, 'runs');
38
+ if (!fs.existsSync(runsDir)) return null;
39
+
40
+ const runs = fs.readdirSync(runsDir)
41
+ .filter(d => d.startsWith('run-'))
42
+ .sort()
43
+ .reverse(); // Latest first
44
+
45
+ for (const runId of runs) {
46
+ const runDir = safeJoin(runsDir, runId);
47
+ const lanesDir = safeJoin(runDir, 'lanes');
48
+
49
+ if (!fs.existsSync(lanesDir)) continue;
50
+
51
+ const laneDirs = fs.readdirSync(lanesDir)
52
+ .filter(f => fs.statSync(safeJoin(lanesDir, f)).isDirectory());
53
+
54
+ if (laneDirs.length === 0) continue;
55
+
56
+ // Check if any lane belongs to this tasks directory
57
+ let matchesTasksDir = false;
58
+ const incompleteLanes: IncompleteLaneInfo[] = [];
59
+ const completedLanes: string[] = [];
60
+
61
+ for (const laneName of laneDirs) {
62
+ const statePath = safeJoin(lanesDir, laneName, 'state.json');
63
+ if (!fs.existsSync(statePath)) continue;
64
+
65
+ const state = loadState<LaneState>(statePath);
66
+ if (!state) continue;
67
+
68
+ // Check if this lane's tasks file is in the target tasks directory
69
+ if (state.tasksFile) {
70
+ const taskFileDir = path.dirname(state.tasksFile);
71
+ if (path.resolve(taskFileDir) === path.resolve(tasksDir)) {
72
+ matchesTasksDir = true;
73
+ }
74
+ }
75
+
76
+ // Check completion status
77
+ if (state.status === 'completed') {
78
+ completedLanes.push(laneName);
79
+ } else {
80
+ // Check if process is alive (zombie detection)
81
+ let isZombie = false;
82
+ if (state.status === 'running' && state.pid) {
83
+ try {
84
+ process.kill(state.pid, 0);
85
+ } catch {
86
+ isZombie = true;
87
+ }
88
+ }
89
+
90
+ incompleteLanes.push({
91
+ name: laneName,
92
+ status: isZombie ? 'zombie' : state.status,
93
+ taskIndex: state.currentTaskIndex,
94
+ totalTasks: state.totalTasks,
95
+ error: state.error || undefined,
96
+ });
97
+ }
98
+ }
99
+
100
+ if (matchesTasksDir && incompleteLanes.length > 0) {
101
+ return {
102
+ runDir,
103
+ runId,
104
+ incompleteLanes,
105
+ completedLanes,
106
+ totalLanes: laneDirs.length,
107
+ };
108
+ }
109
+ }
110
+
111
+ return null;
112
+ }
113
+
114
+ interface RunOptions {
115
+ tasksDir?: string;
116
+ dryRun: boolean;
117
+ executor: string | null;
118
+ maxConcurrent: number | null;
119
+ skipDoctor: boolean;
120
+ skipPreflight: boolean;
121
+ noGit: boolean;
122
+ raw: boolean;
123
+ help: boolean;
124
+ }
125
+
126
+ function printHelp(): void {
127
+ console.log(`
128
+ Usage: cursorflow run <tasks-dir> [options]
129
+
130
+ Run task orchestration based on dependency graph.
131
+
132
+ If an existing run with incomplete lanes is found for the same tasks directory,
133
+ it will automatically resume instead of starting a new run.
134
+
135
+ Options:
136
+ <tasks-dir> Directory containing task JSON files
137
+ --max-concurrent <num> Limit parallel agents (overrides config)
138
+ --executor <type> cursor-agent | cloud
139
+ --skip-doctor Skip environment checks (not recommended)
140
+ --skip-preflight Skip preflight checks (Git remote, etc.)
141
+ --no-git Disable Git operations (worktree, push, commit)
142
+ --raw Save raw logs (absolute raw, no processing)
143
+ --dry-run Show execution plan without starting agents
144
+ --help, -h Show help
145
+
146
+ Examples:
147
+ cursorflow run _cursorflow/tasks
148
+ cursorflow run _cursorflow/tasks --no-git --skip-doctor
149
+ `);
150
+ }
151
+
152
+ function parseArgs(args: string[]): RunOptions {
153
+ const tasksDir = args.find(a => !a.startsWith('--'));
154
+ const executorIdx = args.indexOf('--executor');
155
+ const maxConcurrentIdx = args.indexOf('--max-concurrent');
156
+
157
+ return {
158
+ tasksDir,
159
+ dryRun: args.includes('--dry-run'),
160
+ executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
161
+ maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '0') || null : null,
162
+ skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
163
+ skipPreflight: args.includes('--skip-preflight'),
164
+ noGit: args.includes('--no-git'),
165
+ raw: args.includes('--raw'),
166
+ help: args.includes('--help') || args.includes('-h'),
167
+ };
168
+ }
169
+
170
+ async function run(args: string[]): Promise<void> {
171
+ const options = parseArgs(args);
172
+
173
+ if (options.help) {
174
+ printHelp();
175
+ return;
176
+ }
177
+
178
+ // Auto-setup Cursor commands if missing or outdated
179
+ if (!areCommandsInstalled()) {
180
+ logger.info('Installing missing or outdated Cursor IDE commands...');
181
+ try {
182
+ setupCommands({ silent: true });
183
+ } catch (e) {
184
+ // Non-blocking
185
+ }
186
+ }
187
+
188
+ if (!options.tasksDir) {
189
+ console.log('\nUsage: cursorflow run <tasks-dir> [options]');
190
+ throw new Error('Tasks directory required');
191
+ }
192
+
193
+ const config = loadConfig();
194
+ const logsDir = getLogsDir(config);
195
+ const originalCwd = process.cwd();
196
+
197
+ // Change current directory to project root for consistent path handling
198
+ if (config.projectRoot !== originalCwd) {
199
+ logger.debug(`Changing directory to project root: ${config.projectRoot}`);
200
+ process.chdir(config.projectRoot);
201
+ }
202
+
203
+ // Resolve tasks dir:
204
+ // 1. Prefer the exact path if it exists relative to original cwd
205
+ // 2. Search in flowsDir by name
206
+ // 3. Fall back to projectRoot-relative tasksDir for backward compatibility
207
+ let tasksDir = '';
208
+ if (path.isAbsolute(options.tasksDir)) {
209
+ tasksDir = options.tasksDir;
210
+ } else {
211
+ const relPath = path.resolve(originalCwd, options.tasksDir);
212
+ if (fs.existsSync(relPath)) {
213
+ tasksDir = relPath;
214
+ } else {
215
+ // Try finding in flowsDir
216
+ const flowsDir = safeJoin(config.projectRoot, config.flowsDir);
217
+ const foundFlow = findFlowDir(flowsDir, options.tasksDir);
218
+ if (foundFlow) {
219
+ tasksDir = foundFlow;
220
+ } else {
221
+ // Fallback to legacy tasksDir
222
+ tasksDir = safeJoin(config.projectRoot, options.tasksDir);
223
+ }
224
+ }
225
+ }
226
+
227
+ if (!fs.existsSync(tasksDir)) {
228
+ throw new Error(`Tasks or Flow directory not found: ${options.tasksDir} (resolved to: ${tasksDir})`);
229
+ }
230
+
231
+ // Check for existing incomplete run and auto-resume
232
+ const existingRun = findExistingRunForTasks(logsDir, tasksDir);
233
+ if (existingRun && existingRun.incompleteLanes.length > 0) {
234
+ logger.section('📋 Existing Run Detected');
235
+ logger.info(`Run: ${existingRun.runId}`);
236
+ logger.info(`Completed: ${existingRun.completedLanes.length}/${existingRun.totalLanes} lanes`);
237
+
238
+ console.log('');
239
+ logger.info('Incomplete lanes:');
240
+ for (const lane of existingRun.incompleteLanes) {
241
+ const statusEmoji = lane.status === 'failed' ? '❌' :
242
+ lane.status === 'zombie' ? '🧟' :
243
+ lane.status === 'running' ? '🔄' : '⏸';
244
+ logger.info(` ${statusEmoji} ${lane.name}: ${lane.status} (${lane.taskIndex}/${lane.totalTasks})`);
245
+ if (lane.error) {
246
+ logger.warn(` └─ ${lane.error.substring(0, 60)}${lane.error.length > 60 ? '...' : ''}`);
247
+ }
248
+ }
249
+
250
+ console.log('');
251
+ logger.info('🔄 Auto-resuming from existing run...');
252
+ console.log('');
253
+
254
+ // Call the resume command with --all flag
255
+ const resumeCmd = require('./resume');
256
+ const resumeArgs = [
257
+ '--all',
258
+ '--run-dir', existingRun.runDir,
259
+ ];
260
+
261
+ if (options.skipDoctor) resumeArgs.push('--skip-doctor');
262
+ if (options.skipPreflight) resumeArgs.push('--skip-preflight');
263
+ if (options.noGit) resumeArgs.push('--no-git');
264
+ if (options.executor) {
265
+ resumeArgs.push('--executor', options.executor);
266
+ }
267
+ if (options.maxConcurrent) {
268
+ resumeArgs.push('--max-concurrent', String(options.maxConcurrent));
269
+ }
270
+
271
+ await resumeCmd(resumeArgs);
272
+ return;
273
+ }
274
+
275
+ // Check if doctor has been run at least once
276
+ const doctorStatus = getDoctorStatus(config.projectRoot);
277
+ if (!doctorStatus) {
278
+ logger.warn('It looks like you haven\'t run `cursorflow doctor` yet.');
279
+ logger.warn('Running doctor is highly recommended to catch environment issues early.');
280
+ console.log(' Run: cursorflow doctor\n');
281
+ }
282
+
283
+ // Preflight checks (doctor)
284
+ if (!options.skipDoctor && !options.skipPreflight) {
285
+ const report = runDoctor({
286
+ cwd: process.cwd(),
287
+ tasksDir,
288
+ executor: options.executor || config.executor,
289
+ includeCursorAgentChecks: true,
290
+ });
291
+
292
+ if (!report.ok) {
293
+ logger.section('🛑 Pre-flight check failed');
294
+ for (const issue of report.issues) {
295
+ const header = `${issue.title} (${issue.id})`;
296
+ if (issue.severity === 'error') {
297
+ logger.error(header, { emoji: '❌' });
298
+ } else {
299
+ logger.warn(header, { emoji: '⚠️' });
300
+ }
301
+ console.log(` ${issue.message}`);
302
+ if (issue.details) console.log(` Details: ${issue.details}`);
303
+ if (issue.fixes?.length) {
304
+ console.log(' Fix:');
305
+ for (const fix of issue.fixes) console.log(` - ${fix}`);
306
+ }
307
+ console.log('');
308
+ }
309
+ throw new Error('Pre-flight checks failed. Run `cursorflow doctor` for details.');
310
+ }
311
+ }
312
+
313
+ try {
314
+ await orchestrate(tasksDir, {
315
+ executor: options.executor || config.executor,
316
+ pollInterval: config.pollInterval * 1000,
317
+ runDir: path.join(logsDir, 'runs', `run-${Date.now()}`),
318
+ maxConcurrentLanes: options.maxConcurrent || config.maxConcurrentLanes,
319
+ webhooks: config.webhooks || [],
320
+ enhancedLogging: {
321
+ ...config.enhancedLogging,
322
+ ...(options.raw ? { raw: true } : {}),
323
+ },
324
+ noGit: options.noGit,
325
+ skipPreflight: options.skipPreflight,
326
+ });
327
+ } catch (error: any) {
328
+ // Re-throw to be handled by the main entry point
329
+ throw new Error(`Orchestration failed: ${error.message}`);
330
+ }
331
+ }
332
+
333
+ export = run;