@litmers/cursorflow-orchestrator 0.1.13 → 0.1.15

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 (76) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +83 -2
  3. package/commands/cursorflow-clean.md +20 -6
  4. package/commands/cursorflow-prepare.md +1 -1
  5. package/commands/cursorflow-resume.md +127 -6
  6. package/commands/cursorflow-run.md +2 -2
  7. package/commands/cursorflow-signal.md +11 -4
  8. package/dist/cli/clean.js +164 -12
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +6 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/logs.d.ts +8 -0
  14. package/dist/cli/logs.js +759 -0
  15. package/dist/cli/logs.js.map +1 -0
  16. package/dist/cli/monitor.js +113 -30
  17. package/dist/cli/monitor.js.map +1 -1
  18. package/dist/cli/prepare.js +1 -1
  19. package/dist/cli/resume.js +367 -18
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +9 -0
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/signal.js +34 -20
  24. package/dist/cli/signal.js.map +1 -1
  25. package/dist/core/orchestrator.d.ts +13 -1
  26. package/dist/core/orchestrator.js +396 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.d.ts +2 -0
  29. package/dist/core/reviewer.js +24 -2
  30. package/dist/core/reviewer.js.map +1 -1
  31. package/dist/core/runner.d.ts +9 -3
  32. package/dist/core/runner.js +266 -61
  33. package/dist/core/runner.js.map +1 -1
  34. package/dist/utils/config.js +38 -1
  35. package/dist/utils/config.js.map +1 -1
  36. package/dist/utils/enhanced-logger.d.ts +210 -0
  37. package/dist/utils/enhanced-logger.js +1030 -0
  38. package/dist/utils/enhanced-logger.js.map +1 -0
  39. package/dist/utils/events.d.ts +59 -0
  40. package/dist/utils/events.js +37 -0
  41. package/dist/utils/events.js.map +1 -0
  42. package/dist/utils/git.d.ts +11 -0
  43. package/dist/utils/git.js +40 -0
  44. package/dist/utils/git.js.map +1 -1
  45. package/dist/utils/logger.d.ts +2 -0
  46. package/dist/utils/logger.js +4 -1
  47. package/dist/utils/logger.js.map +1 -1
  48. package/dist/utils/types.d.ts +132 -1
  49. package/dist/utils/webhook.d.ts +5 -0
  50. package/dist/utils/webhook.js +109 -0
  51. package/dist/utils/webhook.js.map +1 -0
  52. package/examples/README.md +1 -1
  53. package/package.json +2 -1
  54. package/scripts/patches/test-cursor-agent.js +1 -1
  55. package/scripts/simple-logging-test.sh +97 -0
  56. package/scripts/test-real-cursor-lifecycle.sh +289 -0
  57. package/scripts/test-real-logging.sh +289 -0
  58. package/scripts/test-streaming-multi-task.sh +247 -0
  59. package/src/cli/clean.ts +170 -13
  60. package/src/cli/index.ts +4 -1
  61. package/src/cli/logs.ts +863 -0
  62. package/src/cli/monitor.ts +123 -30
  63. package/src/cli/prepare.ts +1 -1
  64. package/src/cli/resume.ts +463 -22
  65. package/src/cli/run.ts +10 -0
  66. package/src/cli/signal.ts +43 -27
  67. package/src/core/orchestrator.ts +458 -36
  68. package/src/core/reviewer.ts +40 -4
  69. package/src/core/runner.ts +293 -60
  70. package/src/utils/config.ts +41 -1
  71. package/src/utils/enhanced-logger.ts +1166 -0
  72. package/src/utils/events.ts +117 -0
  73. package/src/utils/git.ts +40 -0
  74. package/src/utils/logger.ts +4 -1
  75. package/src/utils/types.ts +160 -1
  76. package/src/utils/webhook.ts +85 -0
package/src/cli/resume.ts CHANGED
@@ -4,10 +4,10 @@
4
4
 
5
5
  import * as path from 'path';
6
6
  import * as fs from 'fs';
7
- import { spawn } from 'child_process';
7
+ import { spawn, ChildProcess } from 'child_process';
8
8
  import * as logger from '../utils/logger';
9
9
  import { loadConfig, getLogsDir } from '../utils/config';
10
- import { loadState } from '../utils/state';
10
+ import { loadState, listLanesInRun } from '../utils/state';
11
11
  import { LaneState } from '../utils/types';
12
12
  import { runDoctor } from '../utils/doctor';
13
13
 
@@ -17,27 +17,41 @@ interface ResumeOptions {
17
17
  clean: boolean;
18
18
  restart: boolean;
19
19
  skipDoctor: boolean;
20
+ all: boolean;
21
+ status: boolean;
22
+ maxConcurrent: number;
20
23
  help: boolean;
21
24
  }
22
25
 
23
26
  function printHelp(): void {
24
27
  console.log(`
25
- Usage: cursorflow resume <lane> [options]
28
+ Usage: cursorflow resume [lane] [options]
26
29
 
27
- Resume an interrupted or failed lane.
30
+ Resume interrupted or failed lanes.
28
31
 
29
32
  Options:
30
- <lane> Lane name to resume
33
+ <lane> Lane name to resume (single lane mode)
34
+ --all Resume ALL incomplete/failed lanes
35
+ --status Show status of all lanes in the run (no resume)
31
36
  --run-dir <path> Use a specific run directory (default: latest)
37
+ --max-concurrent <n> Max lanes to run in parallel (default: 3)
32
38
  --clean Clean up existing worktree before resuming
33
39
  --restart Restart from the first task (index 0)
34
40
  --skip-doctor Skip environment/branch checks (not recommended)
35
41
  --help, -h Show help
42
+
43
+ Examples:
44
+ cursorflow resume --status # Check status of all lanes
45
+ cursorflow resume --all # Resume all incomplete lanes
46
+ cursorflow resume lane-1 # Resume single lane
47
+ cursorflow resume --all --restart # Restart all incomplete lanes from task 0
48
+ cursorflow resume --all --max-concurrent 2 # Resume with max 2 parallel lanes
36
49
  `);
37
50
  }
38
51
 
39
52
  function parseArgs(args: string[]): ResumeOptions {
40
53
  const runDirIdx = args.indexOf('--run-dir');
54
+ const maxConcurrentIdx = args.indexOf('--max-concurrent');
41
55
 
42
56
  return {
43
57
  lane: args.find(a => !a.startsWith('--')) || null,
@@ -45,6 +59,9 @@ function parseArgs(args: string[]): ResumeOptions {
45
59
  clean: args.includes('--clean'),
46
60
  restart: args.includes('--restart'),
47
61
  skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
62
+ all: args.includes('--all'),
63
+ status: args.includes('--status'),
64
+ maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '3') : 3,
48
65
  help: args.includes('--help') || args.includes('-h'),
49
66
  };
50
67
  }
@@ -64,6 +81,412 @@ function findLatestRunDir(logsDir: string): string | null {
64
81
  return runs.length > 0 ? path.join(runsDir, runs[0]!) : null;
65
82
  }
66
83
 
84
+ /**
85
+ * Status indicator colors
86
+ */
87
+ const STATUS_COLORS: Record<string, string> = {
88
+ completed: '\x1b[32m', // green
89
+ running: '\x1b[36m', // cyan
90
+ pending: '\x1b[33m', // yellow
91
+ failed: '\x1b[31m', // red
92
+ paused: '\x1b[35m', // magenta
93
+ waiting: '\x1b[33m', // yellow
94
+ reviewing: '\x1b[36m', // cyan
95
+ unknown: '\x1b[90m', // gray
96
+ };
97
+ const RESET = '\x1b[0m';
98
+
99
+ interface LaneInfo {
100
+ name: string;
101
+ dir: string;
102
+ state: LaneState | null;
103
+ needsResume: boolean;
104
+ dependsOn: string[];
105
+ isCompleted: boolean;
106
+ }
107
+
108
+ /**
109
+ * Get all lane statuses from a run directory
110
+ */
111
+ function getAllLaneStatuses(runDir: string): LaneInfo[] {
112
+ const lanesDir = path.join(runDir, 'lanes');
113
+ if (!fs.existsSync(lanesDir)) {
114
+ return [];
115
+ }
116
+
117
+ const lanes = fs.readdirSync(lanesDir)
118
+ .filter(f => fs.statSync(path.join(lanesDir, f)).isDirectory())
119
+ .map(name => {
120
+ const dir = path.join(lanesDir, name);
121
+ const statePath = path.join(dir, 'state.json');
122
+ const state = fs.existsSync(statePath) ? loadState<LaneState>(statePath) : null;
123
+
124
+ // Determine if lane needs resume
125
+ const needsResume = state ? (
126
+ state.status === 'failed' ||
127
+ state.status === 'paused' ||
128
+ state.status === 'running' || // If process crashed mid-run
129
+ (state.status === 'pending' && state.currentTaskIndex > 0)
130
+ ) : false;
131
+
132
+ const isCompleted = state?.status === 'completed';
133
+ const dependsOn = state?.dependsOn || [];
134
+
135
+ return { name, dir, state, needsResume, dependsOn, isCompleted };
136
+ });
137
+
138
+ return lanes;
139
+ }
140
+
141
+ /**
142
+ * Check if all dependencies of a lane are completed
143
+ */
144
+ function areDependenciesCompleted(
145
+ lane: LaneInfo,
146
+ allLanes: LaneInfo[],
147
+ completedLanes: Set<string>
148
+ ): boolean {
149
+ if (!lane.dependsOn || lane.dependsOn.length === 0) {
150
+ return true;
151
+ }
152
+
153
+ for (const depName of lane.dependsOn) {
154
+ // Check if dependency is in completed set (already succeeded in this resume session)
155
+ if (completedLanes.has(depName)) {
156
+ continue;
157
+ }
158
+
159
+ // Check if dependency was already completed before this resume
160
+ const depLane = allLanes.find(l => l.name === depName);
161
+ if (!depLane || !depLane.isCompleted) {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ return true;
167
+ }
168
+
169
+ /**
170
+ * Print status of all lanes
171
+ */
172
+ function printAllLaneStatus(runDir: string): { total: number; completed: number; needsResume: number } {
173
+ const lanes = getAllLaneStatuses(runDir);
174
+
175
+ if (lanes.length === 0) {
176
+ logger.warn('No lanes found in this run.');
177
+ return { total: 0, completed: 0, needsResume: 0 };
178
+ }
179
+
180
+ logger.section(`📊 Lane Status (${path.basename(runDir)})`);
181
+ console.log('');
182
+
183
+ // Table header
184
+ console.log(' ' +
185
+ 'Lane'.padEnd(25) +
186
+ 'Status'.padEnd(12) +
187
+ 'Progress'.padEnd(12) +
188
+ 'DependsOn'.padEnd(15) +
189
+ 'Resumable'
190
+ );
191
+ console.log(' ' + '-'.repeat(75));
192
+
193
+ let completedCount = 0;
194
+ let needsResumeCount = 0;
195
+ const completedSet = new Set<string>();
196
+
197
+ // First pass: collect completed lanes
198
+ for (const lane of lanes) {
199
+ if (lane.isCompleted) {
200
+ completedSet.add(lane.name);
201
+ }
202
+ }
203
+
204
+ for (const lane of lanes) {
205
+ const state = lane.state;
206
+ const status = state?.status || 'unknown';
207
+ const color = STATUS_COLORS[status] || STATUS_COLORS.unknown;
208
+ const progress = state ? `${state.currentTaskIndex}/${state.totalTasks}` : '-/-';
209
+ const dependsOnStr = lane.dependsOn.length > 0 ? lane.dependsOn.join(',').substring(0, 12) : '-';
210
+
211
+ // Check if dependencies are met
212
+ const depsCompleted = areDependenciesCompleted(lane, lanes, completedSet);
213
+ const canResume = lane.needsResume && depsCompleted;
214
+ const blockedByDep = lane.needsResume && !depsCompleted;
215
+
216
+ if (status === 'completed') completedCount++;
217
+ if (lane.needsResume) needsResumeCount++;
218
+
219
+ let resumeIndicator = '';
220
+ if (canResume) {
221
+ resumeIndicator = '\x1b[33m✓\x1b[0m';
222
+ } else if (blockedByDep) {
223
+ resumeIndicator = '\x1b[90m⏳ waiting\x1b[0m';
224
+ }
225
+
226
+ console.log(' ' +
227
+ lane.name.padEnd(25) +
228
+ `${color}${status.padEnd(12)}${RESET}` +
229
+ progress.padEnd(12) +
230
+ dependsOnStr.padEnd(15) +
231
+ resumeIndicator
232
+ );
233
+
234
+ // Show error if failed
235
+ if (status === 'failed' && state?.error) {
236
+ console.log(` ${''.padEnd(25)}\x1b[31m└─ ${state.error.substring(0, 50)}${state.error.length > 50 ? '...' : ''}\x1b[0m`);
237
+ }
238
+
239
+ // Show blocked dependency info
240
+ if (blockedByDep) {
241
+ const pendingDeps = lane.dependsOn.filter(d => !completedSet.has(d));
242
+ console.log(` ${''.padEnd(25)}\x1b[90m└─ waiting for: ${pendingDeps.join(', ')}\x1b[0m`);
243
+ }
244
+ }
245
+
246
+ console.log('');
247
+ console.log(` Total: ${lanes.length} | Completed: ${completedCount} | Needs Resume: ${needsResumeCount}`);
248
+
249
+ if (needsResumeCount > 0) {
250
+ console.log('');
251
+ console.log(' \x1b[33mTip:\x1b[0m Run \x1b[32mcursorflow resume --all\x1b[0m to resume all incomplete lanes');
252
+ console.log(' Lanes with dependencies will wait until their dependencies complete.');
253
+ }
254
+
255
+ return { total: lanes.length, completed: completedCount, needsResume: needsResumeCount };
256
+ }
257
+
258
+ /**
259
+ * Resume a single lane and return the child process
260
+ */
261
+ function spawnLaneResume(
262
+ laneName: string,
263
+ laneDir: string,
264
+ state: LaneState,
265
+ options: { restart: boolean }
266
+ ): ChildProcess {
267
+ const runnerPath = require.resolve('../core/runner');
268
+ const startIndex = options.restart ? 0 : state.currentTaskIndex;
269
+
270
+ const runnerArgs = [
271
+ runnerPath,
272
+ state.tasksFile!,
273
+ '--run-dir', laneDir,
274
+ '--start-index', String(startIndex),
275
+ ];
276
+
277
+ const child = spawn('node', runnerArgs, {
278
+ stdio: 'inherit',
279
+ env: process.env,
280
+ });
281
+
282
+ return child;
283
+ }
284
+
285
+ /**
286
+ * Wait for a child process to exit
287
+ */
288
+ function waitForChild(child: ChildProcess): Promise<number> {
289
+ return new Promise((resolve, reject) => {
290
+ child.on('exit', (code) => {
291
+ resolve(code ?? -1);
292
+ });
293
+ child.on('error', (err) => {
294
+ reject(err);
295
+ });
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Resume multiple lanes with concurrency control and dependency awareness
301
+ */
302
+ async function resumeAllLanes(
303
+ runDir: string,
304
+ options: { restart: boolean; maxConcurrent: number; skipDoctor: boolean }
305
+ ): Promise<{ succeeded: string[]; failed: string[]; skipped: string[] }> {
306
+ const allLanes = getAllLaneStatuses(runDir);
307
+ const lanesToResume = allLanes.filter(l => l.needsResume && l.state?.tasksFile);
308
+
309
+ if (lanesToResume.length === 0) {
310
+ logger.success('All lanes are already completed! Nothing to resume.');
311
+ return { succeeded: [], failed: [], skipped: [] };
312
+ }
313
+
314
+ // Check for lanes with unmet dependencies that can never be satisfied
315
+ const completedSet = new Set<string>(allLanes.filter(l => l.isCompleted).map(l => l.name));
316
+ const toResumeNames = new Set<string>(lanesToResume.map(l => l.name));
317
+
318
+ const skippedLanes: string[] = [];
319
+ const resolvableLanes: LaneInfo[] = [];
320
+
321
+ for (const lane of lanesToResume) {
322
+ // Check if all dependencies can be satisfied (either already completed or in the resume list)
323
+ const unmetDeps = lane.dependsOn.filter(dep =>
324
+ !completedSet.has(dep) && !toResumeNames.has(dep)
325
+ );
326
+
327
+ if (unmetDeps.length > 0) {
328
+ logger.warn(`⏭ Skipping ${lane.name}: unresolvable dependencies (${unmetDeps.join(', ')})`);
329
+ skippedLanes.push(lane.name);
330
+ } else {
331
+ resolvableLanes.push(lane);
332
+ }
333
+ }
334
+
335
+ if (resolvableLanes.length === 0) {
336
+ logger.warn('No lanes can be resumed due to dependency constraints.');
337
+ return { succeeded: [], failed: [], skipped: skippedLanes };
338
+ }
339
+
340
+ logger.section(`🔁 Resuming ${resolvableLanes.length} Lane(s)`);
341
+ logger.info(`Max concurrent: ${options.maxConcurrent}`);
342
+ logger.info(`Mode: ${options.restart ? 'Restart from beginning' : 'Continue from last task'}`);
343
+
344
+ // Show dependency order
345
+ const lanesWithDeps = resolvableLanes.filter(l => l.dependsOn.length > 0);
346
+ if (lanesWithDeps.length > 0) {
347
+ logger.info(`Dependency-aware: ${lanesWithDeps.length} lane(s) have dependencies`);
348
+ }
349
+ console.log('');
350
+
351
+ // Run doctor check once if needed (check git status)
352
+ if (!options.skipDoctor) {
353
+ logger.info('Running pre-flight checks...');
354
+
355
+ // Use the first lane's tasksDir for doctor check
356
+ const firstLane = resolvableLanes[0]!;
357
+ const tasksDir = path.dirname(firstLane.state!.tasksFile!);
358
+
359
+ const report = runDoctor({
360
+ cwd: process.cwd(),
361
+ tasksDir,
362
+ includeCursorAgentChecks: false,
363
+ });
364
+
365
+ const blockingIssues = report.issues.filter(i =>
366
+ i.severity === 'error' &&
367
+ (i.id.startsWith('branch.') || i.id.startsWith('git.'))
368
+ );
369
+
370
+ if (blockingIssues.length > 0) {
371
+ logger.section('🛑 Pre-resume check found issues');
372
+ for (const issue of blockingIssues) {
373
+ logger.error(`${issue.title} (${issue.id})`, '❌');
374
+ console.log(` ${issue.message}`);
375
+ }
376
+ throw new Error('Pre-resume checks failed. Use --skip-doctor to bypass.');
377
+ }
378
+ }
379
+
380
+ const succeeded: string[] = [];
381
+ const failed: string[] = [];
382
+
383
+ // Create a mutable set for tracking completed lanes (including those from this session)
384
+ const sessionCompleted = new Set<string>(completedSet);
385
+
386
+ // Queue management with dependency awareness
387
+ const pending = new Set<string>(resolvableLanes.map(l => l.name));
388
+ const active: Map<string, ChildProcess> = new Map();
389
+ const laneMap = new Map<string, LaneInfo>(resolvableLanes.map(l => [l.name, l]));
390
+
391
+ /**
392
+ * Find the next lane that can be started (all dependencies met)
393
+ */
394
+ const findReadyLane = (): LaneInfo | null => {
395
+ for (const laneName of pending) {
396
+ const lane = laneMap.get(laneName)!;
397
+ if (areDependenciesCompleted(lane, allLanes, sessionCompleted)) {
398
+ return lane;
399
+ }
400
+ }
401
+ return null;
402
+ };
403
+
404
+ /**
405
+ * Process lanes with dependency awareness
406
+ */
407
+ const processNext = (): void => {
408
+ while (active.size < options.maxConcurrent) {
409
+ const lane = findReadyLane();
410
+
411
+ if (!lane) {
412
+ // No lane ready to start
413
+ if (pending.size > 0 && active.size === 0) {
414
+ // Deadlock: pending lanes exist but none can start and none are running
415
+ const pendingList = Array.from(pending).join(', ');
416
+ logger.error(`Deadlock detected! Lanes waiting: ${pendingList}`);
417
+ for (const ln of pending) {
418
+ failed.push(ln);
419
+ }
420
+ pending.clear();
421
+ }
422
+ break;
423
+ }
424
+
425
+ pending.delete(lane.name);
426
+
427
+ const depsInfo = lane.dependsOn.length > 0 ? ` (after: ${lane.dependsOn.join(', ')})` : '';
428
+ logger.info(`Starting: ${lane.name} (task ${lane.state!.currentTaskIndex}/${lane.state!.totalTasks})${depsInfo}`);
429
+
430
+ const child = spawnLaneResume(lane.name, lane.dir, lane.state!, {
431
+ restart: options.restart,
432
+ });
433
+
434
+ active.set(lane.name, child);
435
+
436
+ // Handle completion
437
+ waitForChild(child).then(code => {
438
+ active.delete(lane.name);
439
+
440
+ if (code === 0) {
441
+ logger.success(`✓ ${lane.name} completed`);
442
+ succeeded.push(lane.name);
443
+ sessionCompleted.add(lane.name); // Mark as completed for dependency resolution
444
+ } else if (code === 2) {
445
+ logger.warn(`⚠ ${lane.name} blocked on dependency change`);
446
+ failed.push(lane.name);
447
+ } else {
448
+ logger.error(`✗ ${lane.name} failed (exit ${code})`);
449
+ failed.push(lane.name);
450
+ }
451
+
452
+ // Try to start more lanes now that one completed
453
+ processNext();
454
+ }).catch(err => {
455
+ active.delete(lane.name);
456
+ logger.error(`✗ ${lane.name} error: ${err.message}`);
457
+ failed.push(lane.name);
458
+ processNext();
459
+ });
460
+ }
461
+ };
462
+
463
+ // Start initial batch
464
+ processNext();
465
+
466
+ // Wait for all to complete
467
+ while (active.size > 0 || pending.size > 0) {
468
+ await new Promise(resolve => setTimeout(resolve, 1000));
469
+
470
+ // Check if we can start more (in case completion handlers haven't triggered processNext yet)
471
+ if (active.size < options.maxConcurrent && pending.size > 0) {
472
+ processNext();
473
+ }
474
+ }
475
+
476
+ // Summary
477
+ console.log('');
478
+ logger.section('📊 Resume Summary');
479
+ logger.info(`Succeeded: ${succeeded.length}`);
480
+ if (failed.length > 0) {
481
+ logger.error(`Failed: ${failed.length} (${failed.join(', ')})`);
482
+ }
483
+ if (skippedLanes.length > 0) {
484
+ logger.warn(`Skipped: ${skippedLanes.length} (${skippedLanes.join(', ')})`);
485
+ }
486
+
487
+ return { succeeded, failed, skipped: skippedLanes };
488
+ }
489
+
67
490
  async function resume(args: string[]): Promise<void> {
68
491
  const options = parseArgs(args);
69
492
 
@@ -75,17 +498,44 @@ async function resume(args: string[]): Promise<void> {
75
498
  const config = loadConfig();
76
499
  const logsDir = getLogsDir(config);
77
500
 
78
- if (!options.lane) {
79
- throw new Error('Lane name required (e.g., cursorflow resume lane-1)');
80
- }
81
-
501
+ // Find run directory
82
502
  let runDir = options.runDir;
83
503
  if (!runDir) {
84
504
  runDir = findLatestRunDir(logsDir);
85
505
  }
86
506
 
87
507
  if (!runDir || !fs.existsSync(runDir)) {
88
- throw new Error(`Run directory not found: ${runDir || 'latest'}`);
508
+ throw new Error(`Run directory not found: ${runDir || 'latest'}. Have you run any tasks yet?`);
509
+ }
510
+
511
+ // Status mode: just show status and exit
512
+ if (options.status) {
513
+ printAllLaneStatus(runDir);
514
+ return;
515
+ }
516
+
517
+ // All mode: resume all incomplete lanes
518
+ if (options.all) {
519
+ const result = await resumeAllLanes(runDir, {
520
+ restart: options.restart,
521
+ maxConcurrent: options.maxConcurrent,
522
+ skipDoctor: options.skipDoctor,
523
+ });
524
+
525
+ if (result.failed.length > 0) {
526
+ throw new Error(`${result.failed.length} lane(s) failed to complete`);
527
+ }
528
+ return;
529
+ }
530
+
531
+ // Single lane mode (original behavior)
532
+ if (!options.lane) {
533
+ // Show status by default if no lane specified
534
+ printAllLaneStatus(runDir);
535
+ console.log('');
536
+ console.log('Usage: cursorflow resume <lane> [options]');
537
+ console.log(' cursorflow resume --all # Resume all incomplete lanes');
538
+ return;
89
539
  }
90
540
 
91
541
  const laneDir = path.join(runDir, 'lanes', options.lane);
@@ -148,21 +598,12 @@ async function resume(args: string[]): Promise<void> {
148
598
  logger.info(`Tasks: ${state.tasksFile}`);
149
599
  logger.info(`Starting from task index: ${options.restart ? 0 : state.currentTaskIndex}`);
150
600
 
151
- const runnerPath = require.resolve('../core/runner');
152
- const runnerArgs = [
153
- runnerPath,
154
- state.tasksFile,
155
- '--run-dir', laneDir,
156
- '--start-index', options.restart ? '0' : String(state.currentTaskIndex),
157
- ];
601
+ const child = spawnLaneResume(options.lane, laneDir, state, {
602
+ restart: options.restart,
603
+ });
158
604
 
159
605
  logger.info(`Spawning runner process...`);
160
606
 
161
- const child = spawn('node', runnerArgs, {
162
- stdio: 'inherit',
163
- env: process.env,
164
- });
165
-
166
607
  return new Promise((resolve, reject) => {
167
608
  child.on('exit', (code) => {
168
609
  if (code === 0) {
package/src/cli/run.ts CHANGED
@@ -16,6 +16,7 @@ interface RunOptions {
16
16
  executor: string | null;
17
17
  maxConcurrent: number | null;
18
18
  skipDoctor: boolean;
19
+ noGit: boolean;
19
20
  help: boolean;
20
21
  }
21
22
 
@@ -30,8 +31,13 @@ Options:
30
31
  --max-concurrent <num> Limit parallel agents (overrides config)
31
32
  --executor <type> cursor-agent | cloud
32
33
  --skip-doctor Skip environment checks (not recommended)
34
+ --no-git Disable Git operations (worktree, push, commit)
33
35
  --dry-run Show execution plan without starting agents
34
36
  --help, -h Show help
37
+
38
+ Examples:
39
+ cursorflow run _cursorflow/tasks
40
+ cursorflow run _cursorflow/tasks --no-git --skip-doctor
35
41
  `);
36
42
  }
37
43
 
@@ -46,6 +52,7 @@ function parseArgs(args: string[]): RunOptions {
46
52
  executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
47
53
  maxConcurrent: maxConcurrentIdx >= 0 ? parseInt(args[maxConcurrentIdx + 1] || '0') || null : null,
48
54
  skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
55
+ noGit: args.includes('--no-git'),
49
56
  help: args.includes('--help') || args.includes('-h'),
50
57
  };
51
58
  }
@@ -134,6 +141,9 @@ async function run(args: string[]): Promise<void> {
134
141
  pollInterval: config.pollInterval * 1000,
135
142
  runDir: path.join(logsDir, 'runs', `run-${Date.now()}`),
136
143
  maxConcurrentLanes: options.maxConcurrent || config.maxConcurrentLanes,
144
+ webhooks: config.webhooks || [],
145
+ enhancedLogging: config.enhancedLogging,
146
+ noGit: options.noGit,
137
147
  });
138
148
  } catch (error: any) {
139
149
  // Re-throw to be handled by the main entry point