@litmers/cursorflow-orchestrator 0.1.13 → 0.1.14

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 (68) hide show
  1. package/CHANGELOG.md +34 -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 +746 -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 +2 -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 +11 -1
  26. package/dist/core/orchestrator.js +257 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.js +20 -0
  29. package/dist/core/reviewer.js.map +1 -1
  30. package/dist/core/runner.js +113 -13
  31. package/dist/core/runner.js.map +1 -1
  32. package/dist/utils/config.js +34 -0
  33. package/dist/utils/config.js.map +1 -1
  34. package/dist/utils/enhanced-logger.d.ts +209 -0
  35. package/dist/utils/enhanced-logger.js +963 -0
  36. package/dist/utils/enhanced-logger.js.map +1 -0
  37. package/dist/utils/events.d.ts +59 -0
  38. package/dist/utils/events.js +37 -0
  39. package/dist/utils/events.js.map +1 -0
  40. package/dist/utils/git.d.ts +5 -0
  41. package/dist/utils/git.js +25 -0
  42. package/dist/utils/git.js.map +1 -1
  43. package/dist/utils/types.d.ts +122 -1
  44. package/dist/utils/webhook.d.ts +5 -0
  45. package/dist/utils/webhook.js +109 -0
  46. package/dist/utils/webhook.js.map +1 -0
  47. package/examples/README.md +1 -1
  48. package/package.json +1 -1
  49. package/scripts/simple-logging-test.sh +97 -0
  50. package/scripts/test-real-logging.sh +289 -0
  51. package/scripts/test-streaming-multi-task.sh +247 -0
  52. package/src/cli/clean.ts +170 -13
  53. package/src/cli/index.ts +4 -1
  54. package/src/cli/logs.ts +848 -0
  55. package/src/cli/monitor.ts +123 -30
  56. package/src/cli/prepare.ts +1 -1
  57. package/src/cli/resume.ts +463 -22
  58. package/src/cli/run.ts +2 -0
  59. package/src/cli/signal.ts +43 -27
  60. package/src/core/orchestrator.ts +303 -37
  61. package/src/core/reviewer.ts +22 -0
  62. package/src/core/runner.ts +128 -12
  63. package/src/utils/config.ts +36 -0
  64. package/src/utils/enhanced-logger.ts +1097 -0
  65. package/src/utils/events.ts +117 -0
  66. package/src/utils/git.ts +25 -0
  67. package/src/utils/types.ts +150 -1
  68. package/src/utils/webhook.ts +85 -0
@@ -10,31 +10,47 @@ 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 * as git from '../utils/git';
17
+ import { execSync } from 'child_process';
18
+ import { EnhancedLogManager, createLogManager, DEFAULT_LOG_CONFIG } from '../utils/enhanced-logger';
14
19
 
15
20
  export interface LaneInfo {
16
21
  name: string;
17
22
  path: string;
18
23
  dependsOn: string[];
24
+ startIndex?: number; // Current task index to resume from
19
25
  }
20
26
 
21
27
  export interface SpawnLaneResult {
22
28
  child: ChildProcess;
23
29
  logPath: string;
30
+ logManager?: EnhancedLogManager;
24
31
  }
25
32
 
26
33
  /**
27
34
  * Spawn a lane process
28
35
  */
29
- export function spawnLane({ tasksFile, laneRunDir, executor }: {
36
+ export function spawnLane({
37
+ laneName,
38
+ tasksFile,
39
+ laneRunDir,
40
+ executor,
41
+ startIndex = 0,
42
+ pipelineBranch,
43
+ enhancedLogConfig,
44
+ }: {
30
45
  laneName: string;
31
46
  tasksFile: string;
32
47
  laneRunDir: string;
33
48
  executor: string;
49
+ startIndex?: number;
50
+ pipelineBranch?: string;
51
+ enhancedLogConfig?: Partial<EnhancedLogConfig>;
34
52
  }): SpawnLaneResult {
35
53
  fs.mkdirSync(laneRunDir, { recursive: true});
36
- const logPath = path.join(laneRunDir, 'terminal.log');
37
- const logFd = fs.openSync(logPath, 'a');
38
54
 
39
55
  // Use extension-less resolve to handle both .ts (dev) and .js (dist)
40
56
  const runnerPath = require.resolve('./runner');
@@ -44,21 +60,75 @@ export function spawnLane({ tasksFile, laneRunDir, executor }: {
44
60
  tasksFile,
45
61
  '--run-dir', laneRunDir,
46
62
  '--executor', executor,
63
+ '--start-index', startIndex.toString(),
47
64
  ];
65
+
66
+ if (pipelineBranch) {
67
+ args.push('--pipeline-branch', pipelineBranch);
68
+ }
48
69
 
49
- const child = spawn('node', args, {
50
- stdio: ['ignore', logFd, logFd],
51
- env: process.env,
52
- detached: false,
53
- });
70
+ // Create enhanced log manager if enabled
71
+ const logConfig = { ...DEFAULT_LOG_CONFIG, ...enhancedLogConfig };
72
+ let logManager: EnhancedLogManager | undefined;
73
+ let logPath: string;
74
+ let child: ChildProcess;
54
75
 
55
- try {
56
- fs.closeSync(logFd);
57
- } catch {
58
- // Ignore
76
+ // Build environment for child process
77
+ const childEnv = {
78
+ ...process.env,
79
+ };
80
+
81
+ if (logConfig.enabled) {
82
+ logManager = createLogManager(laneRunDir, laneName, logConfig);
83
+ logPath = logManager.getLogPaths().clean;
84
+
85
+ // Spawn with pipe for enhanced logging
86
+ child = spawn('node', args, {
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ env: childEnv,
89
+ detached: false,
90
+ });
91
+
92
+ // Pipe stdout and stderr through enhanced logger
93
+ if (child.stdout) {
94
+ child.stdout.on('data', (data: Buffer) => {
95
+ logManager!.writeStdout(data);
96
+ // Also write to process stdout for real-time visibility
97
+ process.stdout.write(data);
98
+ });
99
+ }
100
+
101
+ if (child.stderr) {
102
+ child.stderr.on('data', (data: Buffer) => {
103
+ logManager!.writeStderr(data);
104
+ // Also write to process stderr for real-time visibility
105
+ process.stderr.write(data);
106
+ });
107
+ }
108
+
109
+ // Close log manager when process exits
110
+ child.on('exit', () => {
111
+ logManager?.close();
112
+ });
113
+ } else {
114
+ // Fallback to simple file logging
115
+ logPath = path.join(laneRunDir, 'terminal.log');
116
+ const logFd = fs.openSync(logPath, 'a');
117
+
118
+ child = spawn('node', args, {
119
+ stdio: ['ignore', logFd, logFd],
120
+ env: childEnv,
121
+ detached: false,
122
+ });
123
+
124
+ try {
125
+ fs.closeSync(logFd);
126
+ } catch {
127
+ // Ignore
128
+ }
59
129
  }
60
130
 
61
- return { child, logPath };
131
+ return { child, logPath, logManager };
62
132
  }
63
133
 
64
134
  /**
@@ -138,6 +208,108 @@ export function printLaneStatus(lanes: LaneInfo[], laneRunDirs: Record<string, s
138
208
  }
139
209
  }
140
210
 
211
+ /**
212
+ * Resolve dependencies for all blocked lanes and sync with all active lanes
213
+ */
214
+ async function resolveAllDependencies(
215
+ blockedLanes: Map<string, DependencyRequestPlan>,
216
+ allLanes: LaneInfo[],
217
+ laneRunDirs: Record<string, string>,
218
+ pipelineBranch: string,
219
+ runRoot: string
220
+ ) {
221
+ // 1. Collect all unique changes and commands from blocked lanes
222
+ const allChanges: string[] = [];
223
+ const allCommands: string[] = [];
224
+
225
+ for (const [, plan] of blockedLanes) {
226
+ if (plan.changes) allChanges.push(...plan.changes);
227
+ if (plan.commands) allCommands.push(...plan.commands);
228
+ }
229
+
230
+ const uniqueChanges = Array.from(new Set(allChanges));
231
+ const uniqueCommands = Array.from(new Set(allCommands));
232
+
233
+ if (uniqueCommands.length === 0) return;
234
+
235
+ // 2. Setup a temporary worktree for resolution if needed, or use the first available one
236
+ const firstLaneName = Array.from(blockedLanes.keys())[0]!;
237
+ const statePath = path.join(laneRunDirs[firstLaneName]!, 'state.json');
238
+ const state = loadState<LaneState>(statePath);
239
+ const worktreeDir = state?.worktreeDir || path.join(runRoot, 'resolution-worktree');
240
+
241
+ if (!fs.existsSync(worktreeDir)) {
242
+ logger.info(`Creating resolution worktree at ${worktreeDir}`);
243
+ git.createWorktree(worktreeDir, pipelineBranch, { baseBranch: 'main' });
244
+ }
245
+
246
+ // 3. Resolve on pipeline branch
247
+ logger.info(`Resolving dependencies on ${pipelineBranch}`);
248
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
249
+
250
+ for (const cmd of uniqueCommands) {
251
+ logger.info(`Running: ${cmd}`);
252
+ try {
253
+ execSync(cmd, { cwd: worktreeDir, stdio: 'inherit' });
254
+ } catch (e: any) {
255
+ throw new Error(`Command failed: ${cmd}. ${e.message}`);
256
+ }
257
+ }
258
+
259
+ try {
260
+ git.runGit(['add', '.'], { cwd: worktreeDir });
261
+ git.runGit(['commit', '-m', `chore: auto-resolve dependencies\n\n${uniqueChanges.join('\n')}`], { cwd: worktreeDir });
262
+
263
+ // Log changed files
264
+ const stats = git.getLastOperationStats(worktreeDir);
265
+ if (stats) {
266
+ logger.info('Changed files:\n' + stats);
267
+ }
268
+
269
+ git.push(pipelineBranch, { cwd: worktreeDir });
270
+ } catch (e) { /* ignore if nothing to commit */ }
271
+
272
+ // 4. Sync ALL active lanes (blocked + pending + running)
273
+ // Since we only call this when running.size === 0, "active" means not completed/failed
274
+ for (const lane of allLanes) {
275
+ const laneDir = laneRunDirs[lane.name];
276
+ if (!laneDir) continue;
277
+
278
+ const laneState = loadState<LaneState>(path.join(laneDir, 'state.json'));
279
+ if (!laneState || laneState.status === 'completed' || laneState.status === 'failed') continue;
280
+
281
+ // Merge pipelineBranch into the lane's current task branch
282
+ const currentIdx = laneState.currentTaskIndex;
283
+ const taskConfig = JSON.parse(fs.readFileSync(lane.path, 'utf8')) as RunnerConfig;
284
+ const task = taskConfig.tasks[currentIdx];
285
+
286
+ if (task) {
287
+ const taskBranch = `${pipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
288
+ logger.info(`Syncing lane ${lane.name} branch ${taskBranch}`);
289
+
290
+ try {
291
+ // If task branch doesn't exist yet, it will be created from pipelineBranch when the lane starts
292
+ if (git.branchExists(taskBranch, { cwd: worktreeDir })) {
293
+ git.runGit(['checkout', taskBranch], { cwd: worktreeDir });
294
+ git.runGit(['merge', pipelineBranch, '--no-edit'], { cwd: worktreeDir });
295
+
296
+ // Log changed files
297
+ const stats = git.getLastOperationStats(worktreeDir);
298
+ if (stats) {
299
+ logger.info(`Sync results for ${lane.name}:\n` + stats);
300
+ }
301
+
302
+ git.push(taskBranch, { cwd: worktreeDir });
303
+ }
304
+ } catch (e: any) {
305
+ logger.warn(`Failed to sync branch ${taskBranch}: ${e.message}`);
306
+ }
307
+ }
308
+ }
309
+
310
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
311
+ }
312
+
141
313
  /**
142
314
  * Run orchestration with dependency management
143
315
  */
@@ -146,6 +318,9 @@ export async function orchestrate(tasksDir: string, options: {
146
318
  executor?: string;
147
319
  pollInterval?: number;
148
320
  maxConcurrentLanes?: number;
321
+ webhooks?: WebhookConfig[];
322
+ autoResolveDependencies?: boolean;
323
+ enhancedLogging?: Partial<EnhancedLogConfig>;
149
324
  } = {}): Promise<{ lanes: LaneInfo[]; exitCodes: Record<string, number>; runRoot: string }> {
150
325
  const lanes = listLaneFiles(tasksDir);
151
326
 
@@ -153,8 +328,36 @@ export async function orchestrate(tasksDir: string, options: {
153
328
  throw new Error(`No lane task files found in ${tasksDir}`);
154
329
  }
155
330
 
156
- const runRoot = options.runDir || `_cursorflow/logs/runs/run-${Date.now()}`;
331
+ const runId = `run-${Date.now()}`;
332
+ const runRoot = options.runDir || `_cursorflow/logs/runs/${runId}`;
157
333
  fs.mkdirSync(runRoot, { recursive: true });
334
+
335
+ const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}`;
336
+
337
+ // Initialize event system
338
+ events.setRunId(runId);
339
+ if (options.webhooks) {
340
+ registerWebhooks(options.webhooks);
341
+ }
342
+
343
+ events.emit('orchestration.started', {
344
+ runId,
345
+ tasksDir,
346
+ laneCount: lanes.length,
347
+ runRoot,
348
+ });
349
+
350
+ const maxConcurrent = options.maxConcurrentLanes || 10;
351
+ const running: Map<string, { child: ChildProcess; logPath: string }> = new Map();
352
+ const exitCodes: Record<string, number> = {};
353
+ const completedLanes = new Set<string>();
354
+ const failedLanes = new Set<string>();
355
+ const blockedLanes: Map<string, DependencyRequestPlan> = new Map();
356
+
357
+ // Track start index for each lane (initially 0)
358
+ for (const lane of lanes) {
359
+ lane.startIndex = 0;
360
+ }
158
361
 
159
362
  const laneRunDirs: Record<string, string> = {};
160
363
  for (const lane of lanes) {
@@ -166,35 +369,34 @@ export async function orchestrate(tasksDir: string, options: {
166
369
  logger.info(`Tasks directory: ${tasksDir}`);
167
370
  logger.info(`Run directory: ${runRoot}`);
168
371
  logger.info(`Lanes: ${lanes.length}`);
169
-
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>();
372
+
373
+ const autoResolve = options.autoResolveDependencies !== false;
175
374
 
176
375
  // Monitor lanes
177
376
  const monitorInterval = setInterval(() => {
178
377
  printLaneStatus(lanes, laneRunDirs);
179
378
  }, options.pollInterval || 60000);
180
379
 
181
- while (completedLanes.size + failedLanes.size < lanes.length) {
380
+ while (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length || (blockedLanes.size > 0 && running.size === 0)) {
182
381
  // 1. Identify lanes ready to start
183
382
  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)) {
383
+ // Not already running or completed or failed or blocked
384
+ if (running.has(lane.name) || completedLanes.has(lane.name) || failedLanes.has(lane.name) || blockedLanes.has(lane.name)) {
186
385
  return false;
187
386
  }
188
387
 
189
388
  // Check dependencies
190
389
  for (const dep of lane.dependsOn) {
191
390
  if (failedLanes.has(dep)) {
192
- // If a dependency failed, this lane fails too
193
- logger.error(`Lane ${lane.name} failed because dependency ${dep} failed`);
391
+ logger.error(`Lane ${lane.name} will not start because dependency ${dep} failed`);
194
392
  failedLanes.add(lane.name);
195
393
  exitCodes[lane.name] = 1;
196
394
  return false;
197
395
  }
396
+ if (blockedLanes.has(dep)) {
397
+ // If a dependency is blocked, wait
398
+ return false;
399
+ }
198
400
  if (!completedLanes.has(dep)) {
199
401
  return false;
200
402
  }
@@ -206,20 +408,27 @@ export async function orchestrate(tasksDir: string, options: {
206
408
  for (const lane of readyToStart) {
207
409
  if (running.size >= maxConcurrent) break;
208
410
 
209
- logger.info(`Lane started: ${lane.name}`);
411
+ logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
210
412
  const spawnResult = spawnLane({
211
413
  laneName: lane.name,
212
414
  tasksFile: lane.path,
213
415
  laneRunDir: laneRunDirs[lane.name]!,
214
416
  executor: options.executor || 'cursor-agent',
417
+ startIndex: lane.startIndex,
418
+ pipelineBranch,
419
+ enhancedLogConfig: options.enhancedLogging,
215
420
  });
216
421
 
217
422
  running.set(lane.name, spawnResult);
423
+ events.emit('lane.started', {
424
+ laneName: lane.name,
425
+ pid: spawnResult.child.pid,
426
+ logPath: spawnResult.logPath,
427
+ });
218
428
  }
219
429
 
220
430
  // 3. Wait for any running lane to finish
221
431
  if (running.size > 0) {
222
- // We need to wait for at least one to finish
223
432
  const promises = Array.from(running.entries()).map(async ([name, { child }]) => {
224
433
  const code = await waitChild(child);
225
434
  return { name, code };
@@ -230,23 +439,72 @@ export async function orchestrate(tasksDir: string, options: {
230
439
  running.delete(finished.name);
231
440
  exitCodes[finished.name] = finished.code;
232
441
 
233
- if (finished.code === 0 || finished.code === 2) {
442
+ if (finished.code === 0) {
234
443
  completedLanes.add(finished.name);
444
+ events.emit('lane.completed', {
445
+ laneName: finished.name,
446
+ exitCode: finished.code,
447
+ });
448
+ } else if (finished.code === 2) {
449
+ // Blocked by dependency
450
+ const statePath = path.join(laneRunDirs[finished.name]!, 'state.json');
451
+ const state = loadState<LaneState>(statePath);
452
+
453
+ if (state && state.dependencyRequest) {
454
+ blockedLanes.set(finished.name, state.dependencyRequest);
455
+ const lane = lanes.find(l => l.name === finished.name);
456
+ if (lane) {
457
+ lane.startIndex = Math.max(0, state.currentTaskIndex - 1); // Task was blocked, retry it
458
+ }
459
+
460
+ events.emit('lane.blocked', {
461
+ laneName: finished.name,
462
+ dependencyRequest: state.dependencyRequest,
463
+ });
464
+ logger.warn(`Lane ${finished.name} is blocked on dependency change request`);
465
+ } else {
466
+ failedLanes.add(finished.name);
467
+ logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
468
+ }
235
469
  } else {
236
470
  failedLanes.add(finished.name);
471
+ events.emit('lane.failed', {
472
+ laneName: finished.name,
473
+ exitCode: finished.code,
474
+ error: 'Process exited with non-zero code',
475
+ });
237
476
  }
238
477
 
239
478
  printLaneStatus(lanes, laneRunDirs);
240
479
  } 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));
480
+ // Nothing running. Are we blocked?
481
+ if (blockedLanes.size > 0 && autoResolve) {
482
+ logger.section('🛠 Auto-Resolving Dependencies');
483
+
484
+ try {
485
+ await resolveAllDependencies(blockedLanes, lanes, laneRunDirs, pipelineBranch, runRoot);
486
+
487
+ // Clear blocked status
488
+ blockedLanes.clear();
489
+ logger.success('Dependencies resolved and synced across all active lanes. Resuming...');
490
+ } catch (error: any) {
491
+ logger.error(`Auto-resolution failed: ${error.message}`);
492
+ // Move blocked to failed
493
+ for (const name of blockedLanes.keys()) {
494
+ failedLanes.add(name);
495
+ }
496
+ blockedLanes.clear();
497
+ }
498
+ } else if (readyToStart.length === 0 && completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length) {
499
+ const remaining = lanes.filter(l => !completedLanes.has(l.name) && !failedLanes.has(l.name) && !blockedLanes.has(l.name));
245
500
  logger.error(`Deadlock detected! Remaining lanes cannot start: ${remaining.map(l => l.name).join(', ')}`);
246
501
  for (const l of remaining) {
247
502
  failedLanes.add(l.name);
248
503
  exitCodes[l.name] = 1;
249
504
  }
505
+ } else {
506
+ // All finished
507
+ break;
250
508
  }
251
509
  }
252
510
  }
@@ -262,17 +520,25 @@ export async function orchestrate(tasksDir: string, options: {
262
520
  process.exit(1);
263
521
  }
264
522
 
265
- // Check for blocked lanes
266
- const blocked = Object.entries(exitCodes)
267
- .filter(([, code]) => code === 2)
268
- .map(([lane]) => lane);
523
+ // Check for blocked lanes (if autoResolve was false)
524
+ const blocked = Array.from(blockedLanes.keys());
269
525
 
270
526
  if (blocked.length > 0) {
271
527
  logger.warn(`Lanes blocked on dependency: ${blocked.join(', ')}`);
272
528
  logger.info('Handle dependency changes manually and resume lanes');
529
+ events.emit('orchestration.failed', {
530
+ error: 'Some lanes blocked on dependency change requests',
531
+ blockedLanes: blocked,
532
+ });
273
533
  process.exit(2);
274
534
  }
275
535
 
276
536
  logger.success('All lanes completed successfully!');
537
+ events.emit('orchestration.completed', {
538
+ runId,
539
+ laneCount: lanes.length,
540
+ completedCount: completedLanes.size,
541
+ failedCount: failedLanes.size,
542
+ });
277
543
  return { lanes, exitCodes, runRoot };
278
544
  }
@@ -8,6 +8,7 @@ import * as logger from '../utils/logger';
8
8
  import { appendLog, createConversationEntry } from '../utils/state';
9
9
  import * as path from 'path';
10
10
  import { ReviewResult, ReviewIssue, TaskResult, RunnerConfig, AgentSendResult } from '../utils/types';
11
+ import { events } from '../utils/events';
11
12
 
12
13
  /**
13
14
  * Build review prompt
@@ -159,6 +160,11 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
159
160
 
160
161
  logger.info(`Reviewing: ${taskResult.taskName}`);
161
162
 
163
+ events.emit('review.started', {
164
+ taskName: taskResult.taskName,
165
+ taskBranch: taskResult.taskBranch,
166
+ });
167
+
162
168
  const reviewChatId = cursorAgentCreateChat();
163
169
  const reviewResult = cursorAgentSend({
164
170
  workspaceDir: worktreeDir,
@@ -178,6 +184,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
178
184
 
179
185
  logger.info(`Review result: ${review.status} (${review.issues?.length || 0} issues)`);
180
186
 
187
+ events.emit('review.completed', {
188
+ taskName: taskResult.taskName,
189
+ status: review.status,
190
+ issueCount: review.issues?.length || 0,
191
+ summary: review.summary,
192
+ });
193
+
181
194
  return review;
182
195
  }
183
196
 
@@ -209,6 +222,10 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
209
222
 
210
223
  if (currentReview.status === 'approved') {
211
224
  logger.success(`Review passed: ${taskResult.taskName} (iteration ${iteration + 1})`);
225
+ events.emit('review.approved', {
226
+ taskName: taskResult.taskName,
227
+ iterations: iteration + 1,
228
+ });
212
229
  return { approved: true, review: currentReview, iterations: iteration + 1 };
213
230
  }
214
231
 
@@ -216,6 +233,11 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
216
233
 
217
234
  if (iteration >= maxIterations) {
218
235
  logger.warn(`Max review iterations (${maxIterations}) reached: ${taskResult.taskName}`);
236
+ events.emit('review.rejected', {
237
+ taskName: taskResult.taskName,
238
+ reason: 'Max iterations reached',
239
+ iterations: iteration,
240
+ });
219
241
  break;
220
242
  }
221
243