@litmers/cursorflow-orchestrator 0.1.31 → 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.
- package/README.md +144 -52
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/dist/cli/add.d.ts +7 -0
- package/dist/cli/add.js +377 -0
- package/dist/cli/add.js.map +1 -0
- package/dist/cli/clean.js +1 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/config.d.ts +7 -0
- package/dist/cli/config.js +181 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +7 -33
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +51 -62
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/new.d.ts +7 -0
- package/dist/cli/new.js +232 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/prepare.js +95 -193
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +11 -47
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +27 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tasks.js +1 -2
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/failure-policy.d.ts +9 -0
- package/dist/core/failure-policy.js +9 -0
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.d.ts +20 -6
- package/dist/core/orchestrator.js +213 -333
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +27 -0
- package/dist/core/runner/agent.js +294 -0
- package/dist/core/runner/agent.js.map +1 -0
- package/dist/core/runner/index.d.ts +5 -0
- package/dist/core/runner/index.js +22 -0
- package/dist/core/runner/index.js.map +1 -0
- package/dist/core/runner/pipeline.d.ts +9 -0
- package/dist/core/runner/pipeline.js +539 -0
- package/dist/core/runner/pipeline.js.map +1 -0
- package/dist/core/runner/prompt.d.ts +25 -0
- package/dist/core/runner/prompt.js +175 -0
- package/dist/core/runner/prompt.js.map +1 -0
- package/dist/core/runner/task.d.ts +26 -0
- package/dist/core/runner/task.js +283 -0
- package/dist/core/runner/task.js.map +1 -0
- package/dist/core/runner/utils.d.ts +37 -0
- package/dist/core/runner/utils.js +161 -0
- package/dist/core/runner/utils.js.map +1 -0
- package/dist/core/runner.d.ts +2 -96
- package/dist/core/runner.js +11 -1136
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +326 -0
- package/dist/core/stall-detection.js +781 -0
- package/dist/core/stall-detection.js.map +1 -0
- package/dist/types/config.d.ts +6 -6
- package/dist/types/flow.d.ts +84 -0
- package/dist/types/flow.js +10 -0
- package/dist/types/flow.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +0 -2
- package/dist/types/logging.d.ts +5 -1
- package/dist/types/task.d.ts +7 -11
- package/dist/utils/config.js +7 -15
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/dependency.d.ts +36 -1
- package/dist/utils/dependency.js +256 -1
- package/dist/utils/dependency.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +238 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +115 -5
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/state.js +0 -2
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +2 -2
- package/dist/utils/task-service.js +40 -31
- package/dist/utils/task-service.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/add.ts +397 -0
- package/src/cli/clean.ts +1 -0
- package/src/cli/config.ts +177 -0
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +7 -31
- package/src/cli/monitor.ts +55 -71
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +13 -56
- package/src/cli/run.ts +311 -306
- package/src/cli/tasks.ts +1 -2
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +277 -378
- package/src/core/runner/agent.ts +314 -0
- package/src/core/runner/index.ts +6 -0
- package/src/core/runner/pipeline.ts +567 -0
- package/src/core/runner/prompt.ts +174 -0
- package/src/core/runner/task.ts +320 -0
- package/src/core/runner/utils.ts +142 -0
- package/src/core/runner.ts +8 -1347
- package/src/core/stall-detection.ts +936 -0
- package/src/types/config.ts +6 -6
- package/src/types/flow.ts +91 -0
- package/src/types/index.ts +15 -3
- package/src/types/lane.ts +0 -2
- package/src/types/logging.ts +5 -1
- package/src/types/task.ts +7 -11
- package/src/utils/config.ts +8 -16
- package/src/utils/dependency.ts +311 -2
- package/src/utils/enhanced-logger.ts +263 -927
- package/src/utils/git.ts +145 -5
- package/src/utils/state.ts +0 -2
- package/src/utils/task-service.ts +48 -40
- package/commands/cursorflow-review.md +0 -56
- package/commands/cursorflow-runs.md +0 -59
- package/dist/cli/runs.d.ts +0 -5
- package/dist/cli/runs.js +0 -214
- package/dist/cli/runs.js.map +0 -1
- package/dist/core/reviewer.d.ts +0 -66
- package/dist/core/reviewer.js +0 -265
- package/dist/core/reviewer.js.map +0 -1
- package/src/cli/runs.ts +0 -212
- 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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
--
|
|
137
|
-
--
|
|
138
|
-
--
|
|
139
|
-
--
|
|
140
|
-
--
|
|
141
|
-
--
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
logger.info(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (options.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
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}
|
|
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)
|