@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
@@ -10,31 +10,56 @@ import { spawn, ChildProcess } from 'child_process';
10
10
 
11
11
  import * as logger from '../utils/logger';
12
12
  import { loadState } from '../utils/state';
13
- import { LaneState, RunnerConfig } from '../utils/types';
13
+ import { LaneState, RunnerConfig, WebhookConfig, DependencyRequestPlan, EnhancedLogConfig } from '../utils/types';
14
+ import { events } from '../utils/events';
15
+ import { registerWebhooks } from '../utils/webhook';
16
+ import { loadConfig, getLogsDir } from '../utils/config';
17
+ import * as git from '../utils/git';
18
+ import { execSync } from 'child_process';
19
+ import {
20
+ EnhancedLogManager,
21
+ createLogManager,
22
+ DEFAULT_LOG_CONFIG,
23
+ ParsedMessage,
24
+ stripAnsi
25
+ } from '../utils/enhanced-logger';
14
26
 
15
27
  export interface LaneInfo {
16
28
  name: string;
17
29
  path: string;
18
30
  dependsOn: string[];
31
+ startIndex?: number; // Current task index to resume from
19
32
  }
20
33
 
21
34
  export interface SpawnLaneResult {
22
35
  child: ChildProcess;
23
36
  logPath: string;
37
+ logManager?: EnhancedLogManager;
24
38
  }
25
39
 
26
40
  /**
27
41
  * Spawn a lane process
28
42
  */
29
- export function spawnLane({ tasksFile, laneRunDir, executor }: {
43
+ export function spawnLane({
44
+ laneName,
45
+ tasksFile,
46
+ laneRunDir,
47
+ executor,
48
+ startIndex = 0,
49
+ pipelineBranch,
50
+ enhancedLogConfig,
51
+ noGit = false,
52
+ }: {
30
53
  laneName: string;
31
54
  tasksFile: string;
32
55
  laneRunDir: string;
33
56
  executor: string;
57
+ startIndex?: number;
58
+ pipelineBranch?: string;
59
+ enhancedLogConfig?: Partial<EnhancedLogConfig>;
60
+ noGit?: boolean;
34
61
  }): SpawnLaneResult {
35
62
  fs.mkdirSync(laneRunDir, { recursive: true});
36
- const logPath = path.join(laneRunDir, 'terminal.log');
37
- const logFd = fs.openSync(logPath, 'a');
38
63
 
39
64
  // Use extension-less resolve to handle both .ts (dev) and .js (dist)
40
65
  const runnerPath = require.resolve('./runner');
@@ -44,21 +69,194 @@ export function spawnLane({ tasksFile, laneRunDir, executor }: {
44
69
  tasksFile,
45
70
  '--run-dir', laneRunDir,
46
71
  '--executor', executor,
72
+ '--start-index', startIndex.toString(),
47
73
  ];
74
+
75
+ if (pipelineBranch) {
76
+ args.push('--pipeline-branch', pipelineBranch);
77
+ }
48
78
 
49
- const child = spawn('node', args, {
50
- stdio: ['ignore', logFd, logFd],
51
- env: process.env,
52
- detached: false,
53
- });
79
+ if (noGit) {
80
+ args.push('--no-git');
81
+ }
54
82
 
55
- try {
56
- fs.closeSync(logFd);
57
- } catch {
58
- // Ignore
83
+ // Create enhanced log manager if enabled
84
+ const logConfig = { ...DEFAULT_LOG_CONFIG, ...enhancedLogConfig };
85
+ let logManager: EnhancedLogManager | undefined;
86
+ let logPath: string;
87
+ let child: ChildProcess;
88
+
89
+ // Build environment for child process
90
+ const childEnv = {
91
+ ...process.env,
92
+ };
93
+
94
+ if (logConfig.enabled) {
95
+ // Create callback for clean console output
96
+ const onParsedMessage = (msg: ParsedMessage) => {
97
+ // Print a clean, colored version of the message to the console
98
+ const ts = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false });
99
+ const laneLabel = `[${laneName}]`.padEnd(12);
100
+
101
+ let prefix = '';
102
+ let content = msg.content;
103
+
104
+ switch (msg.type) {
105
+ case 'user':
106
+ prefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
107
+ // No truncation for user prompt to ensure full command visibility
108
+ content = content.replace(/\n/g, ' ');
109
+ break;
110
+ case 'assistant':
111
+ prefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
112
+ break;
113
+ case 'tool':
114
+ prefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
115
+ // Simplify tool call: [Tool: read_file] {"target_file":"..."} -> read_file(target_file: ...)
116
+ const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
117
+ if (toolMatch) {
118
+ const [, name, args] = toolMatch;
119
+ try {
120
+ const parsedArgs = JSON.parse(args!);
121
+ let argStr = '';
122
+
123
+ if (name === 'read_file' && parsedArgs.target_file) {
124
+ argStr = parsedArgs.target_file;
125
+ } else if (name === 'run_terminal_cmd' && parsedArgs.command) {
126
+ argStr = parsedArgs.command;
127
+ } else if (name === 'write' && parsedArgs.file_path) {
128
+ argStr = parsedArgs.file_path;
129
+ } else if (name === 'search_replace' && parsedArgs.file_path) {
130
+ argStr = parsedArgs.file_path;
131
+ } else {
132
+ // Generic summary for other tools
133
+ const keys = Object.keys(parsedArgs);
134
+ if (keys.length > 0) {
135
+ argStr = String(parsedArgs[keys[0]]).substring(0, 50);
136
+ }
137
+ }
138
+ content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
139
+ } catch {
140
+ content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
141
+ }
142
+ }
143
+ break;
144
+ case 'tool_result':
145
+ prefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
146
+ // Simplify tool result: [Tool Result: read_file] ... -> read_file OK
147
+ const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
148
+ content = resMatch ? `${resMatch[1]} OK` : 'result';
149
+ break;
150
+ case 'result':
151
+ prefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
152
+ break;
153
+ case 'system':
154
+ prefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
155
+ break;
156
+ }
157
+
158
+ if (prefix) {
159
+ const lines = content.split('\n');
160
+ const tsPrefix = `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneLabel}${logger.COLORS.reset}`;
161
+
162
+ if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'result') {
163
+ const header = `${prefix} ┌${'─'.repeat(60)}`;
164
+ process.stdout.write(`${tsPrefix} ${header}\n`);
165
+ for (const line of lines) {
166
+ process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} │ ${line}\n`);
167
+ }
168
+ process.stdout.write(`${tsPrefix} ${' '.repeat(stripAnsi(prefix).length)} └${'─'.repeat(60)}\n`);
169
+ } else {
170
+ process.stdout.write(`${tsPrefix} ${prefix} ${content}\n`);
171
+ }
172
+ }
173
+ };
174
+
175
+ logManager = createLogManager(laneRunDir, laneName, logConfig, onParsedMessage);
176
+ logPath = logManager.getLogPaths().clean;
177
+
178
+ // Spawn with pipe for enhanced logging
179
+ child = spawn('node', args, {
180
+ stdio: ['ignore', 'pipe', 'pipe'],
181
+ env: childEnv,
182
+ detached: false,
183
+ });
184
+
185
+ // Buffer for non-JSON lines
186
+ let lineBuffer = '';
187
+
188
+ // Pipe stdout and stderr through enhanced logger
189
+ if (child.stdout) {
190
+ child.stdout.on('data', (data: Buffer) => {
191
+ logManager!.writeStdout(data);
192
+
193
+ // Filter out JSON lines from console output to keep it clean
194
+ const str = data.toString();
195
+ lineBuffer += str;
196
+ const lines = lineBuffer.split('\n');
197
+ lineBuffer = lines.pop() || '';
198
+
199
+ for (const line of lines) {
200
+ const trimmed = line.trim();
201
+ // Only print if NOT a noisy line
202
+ if (trimmed &&
203
+ !trimmed.startsWith('{') &&
204
+ !trimmed.startsWith('[') &&
205
+ !trimmed.includes('{"type"') &&
206
+ !trimmed.includes('Heartbeat:')) {
207
+ process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneName.padEnd(10)}${logger.COLORS.reset} ${line}\n`);
208
+ }
209
+ }
210
+ });
211
+ }
212
+
213
+ if (child.stderr) {
214
+ child.stderr.on('data', (data: Buffer) => {
215
+ logManager!.writeStderr(data);
216
+ const str = data.toString();
217
+ const lines = str.split('\n');
218
+ for (const line of lines) {
219
+ const trimmed = line.trim();
220
+ if (trimmed) {
221
+ // Check if it's a real error or just git/status output on stderr
222
+ const isStatus = trimmed.startsWith('Preparing worktree') ||
223
+ trimmed.startsWith('Switched to a new branch') ||
224
+ trimmed.startsWith('HEAD is now at') ||
225
+ trimmed.includes('actual output');
226
+
227
+ if (isStatus) {
228
+ process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneName.padEnd(10)}${logger.COLORS.reset} ${trimmed}\n`);
229
+ } else {
230
+ process.stderr.write(`${logger.COLORS.red}[${laneName}] ERROR: ${trimmed}${logger.COLORS.reset}\n`);
231
+ }
232
+ }
233
+ }
234
+ });
235
+ }
236
+
237
+ // Close log manager when process exits
238
+ child.on('exit', () => {
239
+ logManager?.close();
240
+ });
241
+ } else {
242
+ // Fallback to simple file logging
243
+ logPath = path.join(laneRunDir, 'terminal.log');
244
+ const logFd = fs.openSync(logPath, 'a');
245
+
246
+ child = spawn('node', args, {
247
+ stdio: ['ignore', logFd, logFd],
248
+ env: childEnv,
249
+ detached: false,
250
+ });
251
+
252
+ try {
253
+ fs.closeSync(logFd);
254
+ } catch {
255
+ // Ignore
256
+ }
59
257
  }
60
258
 
61
- return { child, logPath };
259
+ return { child, logPath, logManager };
62
260
  }
63
261
 
64
262
  /**
@@ -138,6 +336,108 @@ export function printLaneStatus(lanes: LaneInfo[], laneRunDirs: Record<string, s
138
336
  }
139
337
  }
140
338
 
339
+ /**
340
+ * Resolve dependencies for all blocked lanes and sync with all active lanes
341
+ */
342
+ async function resolveAllDependencies(
343
+ blockedLanes: Map<string, DependencyRequestPlan>,
344
+ allLanes: LaneInfo[],
345
+ laneRunDirs: Record<string, string>,
346
+ pipelineBranch: string,
347
+ runRoot: string
348
+ ) {
349
+ // 1. Collect all unique changes and commands from blocked lanes
350
+ const allChanges: string[] = [];
351
+ const allCommands: string[] = [];
352
+
353
+ for (const [, plan] of blockedLanes) {
354
+ if (plan.changes) allChanges.push(...plan.changes);
355
+ if (plan.commands) allCommands.push(...plan.commands);
356
+ }
357
+
358
+ const uniqueChanges = Array.from(new Set(allChanges));
359
+ const uniqueCommands = Array.from(new Set(allCommands));
360
+
361
+ if (uniqueCommands.length === 0) return;
362
+
363
+ // 2. Setup a temporary worktree for resolution if needed, or use the first available one
364
+ const firstLaneName = Array.from(blockedLanes.keys())[0]!;
365
+ const statePath = path.join(laneRunDirs[firstLaneName]!, 'state.json');
366
+ const state = loadState<LaneState>(statePath);
367
+ const worktreeDir = state?.worktreeDir || path.join(runRoot, 'resolution-worktree');
368
+
369
+ if (!fs.existsSync(worktreeDir)) {
370
+ logger.info(`Creating resolution worktree at ${worktreeDir}`);
371
+ git.createWorktree(worktreeDir, pipelineBranch, { baseBranch: 'main' });
372
+ }
373
+
374
+ // 3. Resolve on pipeline branch
375
+ logger.info(`Resolving dependencies on ${pipelineBranch}`);
376
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
377
+
378
+ for (const cmd of uniqueCommands) {
379
+ logger.info(`Running: ${cmd}`);
380
+ try {
381
+ execSync(cmd, { cwd: worktreeDir, stdio: 'inherit' });
382
+ } catch (e: any) {
383
+ throw new Error(`Command failed: ${cmd}. ${e.message}`);
384
+ }
385
+ }
386
+
387
+ try {
388
+ git.runGit(['add', '.'], { cwd: worktreeDir });
389
+ git.runGit(['commit', '-m', `chore: auto-resolve dependencies\n\n${uniqueChanges.join('\n')}`], { cwd: worktreeDir });
390
+
391
+ // Log changed files
392
+ const stats = git.getLastOperationStats(worktreeDir);
393
+ if (stats) {
394
+ logger.info('Changed files:\n' + stats);
395
+ }
396
+
397
+ git.push(pipelineBranch, { cwd: worktreeDir });
398
+ } catch (e) { /* ignore if nothing to commit */ }
399
+
400
+ // 4. Sync ALL active lanes (blocked + pending + running)
401
+ // Since we only call this when running.size === 0, "active" means not completed/failed
402
+ for (const lane of allLanes) {
403
+ const laneDir = laneRunDirs[lane.name];
404
+ if (!laneDir) continue;
405
+
406
+ const laneState = loadState<LaneState>(path.join(laneDir, 'state.json'));
407
+ if (!laneState || laneState.status === 'completed' || laneState.status === 'failed') continue;
408
+
409
+ // Merge pipelineBranch into the lane's current task branch
410
+ const currentIdx = laneState.currentTaskIndex;
411
+ const taskConfig = JSON.parse(fs.readFileSync(lane.path, 'utf8')) as RunnerConfig;
412
+ const task = taskConfig.tasks[currentIdx];
413
+
414
+ if (task) {
415
+ const taskBranch = `${pipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
416
+ logger.info(`Syncing lane ${lane.name} branch ${taskBranch}`);
417
+
418
+ try {
419
+ // If task branch doesn't exist yet, it will be created from pipelineBranch when the lane starts
420
+ if (git.branchExists(taskBranch, { cwd: worktreeDir })) {
421
+ git.runGit(['checkout', taskBranch], { cwd: worktreeDir });
422
+ git.runGit(['merge', pipelineBranch, '--no-edit'], { cwd: worktreeDir });
423
+
424
+ // Log changed files
425
+ const stats = git.getLastOperationStats(worktreeDir);
426
+ if (stats) {
427
+ logger.info(`Sync results for ${lane.name}:\n` + stats);
428
+ }
429
+
430
+ git.push(taskBranch, { cwd: worktreeDir });
431
+ }
432
+ } catch (e: any) {
433
+ logger.warn(`Failed to sync branch ${taskBranch}: ${e.message}`);
434
+ }
435
+ }
436
+ }
437
+
438
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
439
+ }
440
+
141
441
  /**
142
442
  * Run orchestration with dependency management
143
443
  */
@@ -146,6 +446,10 @@ export async function orchestrate(tasksDir: string, options: {
146
446
  executor?: string;
147
447
  pollInterval?: number;
148
448
  maxConcurrentLanes?: number;
449
+ webhooks?: WebhookConfig[];
450
+ autoResolveDependencies?: boolean;
451
+ enhancedLogging?: Partial<EnhancedLogConfig>;
452
+ noGit?: boolean;
149
453
  } = {}): Promise<{ lanes: LaneInfo[]; exitCodes: Record<string, number>; runRoot: string }> {
150
454
  const lanes = listLaneFiles(tasksDir);
151
455
 
@@ -153,8 +457,42 @@ export async function orchestrate(tasksDir: string, options: {
153
457
  throw new Error(`No lane task files found in ${tasksDir}`);
154
458
  }
155
459
 
156
- const runRoot = options.runDir || `_cursorflow/logs/runs/run-${Date.now()}`;
460
+ const config = loadConfig();
461
+ const logsDir = getLogsDir(config);
462
+ const runId = `run-${Date.now()}`;
463
+ // Use absolute path for runRoot to avoid issues with subfolders
464
+ const runRoot = options.runDir
465
+ ? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir))
466
+ : path.join(logsDir, 'runs', runId);
467
+
157
468
  fs.mkdirSync(runRoot, { recursive: true });
469
+
470
+ const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}`;
471
+
472
+ // Initialize event system
473
+ events.setRunId(runId);
474
+ if (options.webhooks) {
475
+ registerWebhooks(options.webhooks);
476
+ }
477
+
478
+ events.emit('orchestration.started', {
479
+ runId,
480
+ tasksDir,
481
+ laneCount: lanes.length,
482
+ runRoot,
483
+ });
484
+
485
+ const maxConcurrent = options.maxConcurrentLanes || 10;
486
+ const running: Map<string, { child: ChildProcess; logPath: string }> = new Map();
487
+ const exitCodes: Record<string, number> = {};
488
+ const completedLanes = new Set<string>();
489
+ const failedLanes = new Set<string>();
490
+ const blockedLanes: Map<string, DependencyRequestPlan> = new Map();
491
+
492
+ // Track start index for each lane (initially 0)
493
+ for (const lane of lanes) {
494
+ lane.startIndex = 0;
495
+ }
158
496
 
159
497
  const laneRunDirs: Record<string, string> = {};
160
498
  for (const lane of lanes) {
@@ -166,35 +504,54 @@ export async function orchestrate(tasksDir: string, options: {
166
504
  logger.info(`Tasks directory: ${tasksDir}`);
167
505
  logger.info(`Run directory: ${runRoot}`);
168
506
  logger.info(`Lanes: ${lanes.length}`);
507
+
508
+ // Display dependency graph
509
+ logger.info('\n📊 Dependency Graph:');
510
+ for (const lane of lanes) {
511
+ const deps = lane.dependsOn.length > 0 ? ` [depends on: ${lane.dependsOn.join(', ')}]` : '';
512
+ console.log(` ${logger.COLORS.cyan}${lane.name}${logger.COLORS.reset}${deps}`);
513
+
514
+ // Simple tree-like visualization for deep dependencies
515
+ if (lane.dependsOn.length > 0) {
516
+ for (const dep of lane.dependsOn) {
517
+ console.log(` └─ ${dep}`);
518
+ }
519
+ }
520
+ }
521
+ console.log('');
522
+
523
+ // Disable auto-resolve when noGit mode is enabled
524
+ const autoResolve = !options.noGit && options.autoResolveDependencies !== false;
169
525
 
170
- const maxConcurrent = options.maxConcurrentLanes || 10;
171
- const running: Map<string, { child: ChildProcess; logPath: string }> = new Map();
172
- const exitCodes: Record<string, number> = {};
173
- const completedLanes = new Set<string>();
174
- const failedLanes = new Set<string>();
526
+ if (options.noGit) {
527
+ logger.info('🚫 Git operations disabled (--no-git mode)');
528
+ }
175
529
 
176
530
  // Monitor lanes
177
531
  const monitorInterval = setInterval(() => {
178
532
  printLaneStatus(lanes, laneRunDirs);
179
533
  }, options.pollInterval || 60000);
180
534
 
181
- while (completedLanes.size + failedLanes.size < lanes.length) {
535
+ while (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length || (blockedLanes.size > 0 && running.size === 0)) {
182
536
  // 1. Identify lanes ready to start
183
537
  const readyToStart = lanes.filter(lane => {
184
- // Not already running or completed
185
- if (running.has(lane.name) || completedLanes.has(lane.name) || failedLanes.has(lane.name)) {
538
+ // Not already running or completed or failed or blocked
539
+ if (running.has(lane.name) || completedLanes.has(lane.name) || failedLanes.has(lane.name) || blockedLanes.has(lane.name)) {
186
540
  return false;
187
541
  }
188
542
 
189
543
  // Check dependencies
190
544
  for (const dep of lane.dependsOn) {
191
545
  if (failedLanes.has(dep)) {
192
- // If a dependency failed, this lane fails too
193
- logger.error(`Lane ${lane.name} failed because dependency ${dep} failed`);
546
+ logger.error(`Lane ${lane.name} will not start because dependency ${dep} failed`);
194
547
  failedLanes.add(lane.name);
195
548
  exitCodes[lane.name] = 1;
196
549
  return false;
197
550
  }
551
+ if (blockedLanes.has(dep)) {
552
+ // If a dependency is blocked, wait
553
+ return false;
554
+ }
198
555
  if (!completedLanes.has(dep)) {
199
556
  return false;
200
557
  }
@@ -206,20 +563,28 @@ export async function orchestrate(tasksDir: string, options: {
206
563
  for (const lane of readyToStart) {
207
564
  if (running.size >= maxConcurrent) break;
208
565
 
209
- logger.info(`Lane started: ${lane.name}`);
566
+ logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
210
567
  const spawnResult = spawnLane({
211
568
  laneName: lane.name,
212
569
  tasksFile: lane.path,
213
570
  laneRunDir: laneRunDirs[lane.name]!,
214
571
  executor: options.executor || 'cursor-agent',
572
+ startIndex: lane.startIndex,
573
+ pipelineBranch,
574
+ enhancedLogConfig: options.enhancedLogging,
575
+ noGit: options.noGit,
215
576
  });
216
577
 
217
578
  running.set(lane.name, spawnResult);
579
+ events.emit('lane.started', {
580
+ laneName: lane.name,
581
+ pid: spawnResult.child.pid,
582
+ logPath: spawnResult.logPath,
583
+ });
218
584
  }
219
585
 
220
586
  // 3. Wait for any running lane to finish
221
587
  if (running.size > 0) {
222
- // We need to wait for at least one to finish
223
588
  const promises = Array.from(running.entries()).map(async ([name, { child }]) => {
224
589
  const code = await waitChild(child);
225
590
  return { name, code };
@@ -230,23 +595,72 @@ export async function orchestrate(tasksDir: string, options: {
230
595
  running.delete(finished.name);
231
596
  exitCodes[finished.name] = finished.code;
232
597
 
233
- if (finished.code === 0 || finished.code === 2) {
598
+ if (finished.code === 0) {
234
599
  completedLanes.add(finished.name);
600
+ events.emit('lane.completed', {
601
+ laneName: finished.name,
602
+ exitCode: finished.code,
603
+ });
604
+ } else if (finished.code === 2) {
605
+ // Blocked by dependency
606
+ const statePath = path.join(laneRunDirs[finished.name]!, 'state.json');
607
+ const state = loadState<LaneState>(statePath);
608
+
609
+ if (state && state.dependencyRequest) {
610
+ blockedLanes.set(finished.name, state.dependencyRequest);
611
+ const lane = lanes.find(l => l.name === finished.name);
612
+ if (lane) {
613
+ lane.startIndex = Math.max(0, state.currentTaskIndex - 1); // Task was blocked, retry it
614
+ }
615
+
616
+ events.emit('lane.blocked', {
617
+ laneName: finished.name,
618
+ dependencyRequest: state.dependencyRequest,
619
+ });
620
+ logger.warn(`Lane ${finished.name} is blocked on dependency change request`);
621
+ } else {
622
+ failedLanes.add(finished.name);
623
+ logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
624
+ }
235
625
  } else {
236
626
  failedLanes.add(finished.name);
627
+ events.emit('lane.failed', {
628
+ laneName: finished.name,
629
+ exitCode: finished.code,
630
+ error: 'Process exited with non-zero code',
631
+ });
237
632
  }
238
633
 
239
634
  printLaneStatus(lanes, laneRunDirs);
240
635
  } else {
241
- // Nothing running and nothing ready (but not all finished)
242
- // This could happen if there's a circular dependency or some logic error
243
- if (readyToStart.length === 0 && completedLanes.size + failedLanes.size < lanes.length) {
244
- const remaining = lanes.filter(l => !completedLanes.has(l.name) && !failedLanes.has(l.name));
636
+ // Nothing running. Are we blocked?
637
+ if (blockedLanes.size > 0 && autoResolve) {
638
+ logger.section('🛠 Auto-Resolving Dependencies');
639
+
640
+ try {
641
+ await resolveAllDependencies(blockedLanes, lanes, laneRunDirs, pipelineBranch, runRoot);
642
+
643
+ // Clear blocked status
644
+ blockedLanes.clear();
645
+ logger.success('Dependencies resolved and synced across all active lanes. Resuming...');
646
+ } catch (error: any) {
647
+ logger.error(`Auto-resolution failed: ${error.message}`);
648
+ // Move blocked to failed
649
+ for (const name of blockedLanes.keys()) {
650
+ failedLanes.add(name);
651
+ }
652
+ blockedLanes.clear();
653
+ }
654
+ } else if (readyToStart.length === 0 && completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length) {
655
+ const remaining = lanes.filter(l => !completedLanes.has(l.name) && !failedLanes.has(l.name) && !blockedLanes.has(l.name));
245
656
  logger.error(`Deadlock detected! Remaining lanes cannot start: ${remaining.map(l => l.name).join(', ')}`);
246
657
  for (const l of remaining) {
247
658
  failedLanes.add(l.name);
248
659
  exitCodes[l.name] = 1;
249
660
  }
661
+ } else {
662
+ // All finished
663
+ break;
250
664
  }
251
665
  }
252
666
  }
@@ -262,17 +676,25 @@ export async function orchestrate(tasksDir: string, options: {
262
676
  process.exit(1);
263
677
  }
264
678
 
265
- // Check for blocked lanes
266
- const blocked = Object.entries(exitCodes)
267
- .filter(([, code]) => code === 2)
268
- .map(([lane]) => lane);
679
+ // Check for blocked lanes (if autoResolve was false)
680
+ const blocked = Array.from(blockedLanes.keys());
269
681
 
270
682
  if (blocked.length > 0) {
271
683
  logger.warn(`Lanes blocked on dependency: ${blocked.join(', ')}`);
272
684
  logger.info('Handle dependency changes manually and resume lanes');
685
+ events.emit('orchestration.failed', {
686
+ error: 'Some lanes blocked on dependency change requests',
687
+ blockedLanes: blocked,
688
+ });
273
689
  process.exit(2);
274
690
  }
275
691
 
276
692
  logger.success('All lanes completed successfully!');
693
+ events.emit('orchestration.completed', {
694
+ runId,
695
+ laneCount: lanes.length,
696
+ completedCount: completedLanes.size,
697
+ failedCount: failedLanes.size,
698
+ });
277
699
  return { lanes, exitCodes, runRoot };
278
700
  }