@litmers/cursorflow-orchestrator 0.1.30 → 0.1.34

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