@litmers/cursorflow-orchestrator 0.1.39 → 0.2.2

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 (214) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/README.md +20 -16
  3. package/commands/cursorflow-init.md +0 -4
  4. package/dist/cli/logs.js +108 -9
  5. package/dist/cli/logs.js.map +1 -1
  6. package/dist/cli/models.js +20 -3
  7. package/dist/cli/models.js.map +1 -1
  8. package/dist/cli/monitor.d.ts +7 -10
  9. package/dist/cli/monitor.js +1088 -1240
  10. package/dist/cli/monitor.js.map +1 -1
  11. package/dist/cli/prepare.js +0 -1
  12. package/dist/cli/prepare.js.map +1 -1
  13. package/dist/cli/resume.js +23 -5
  14. package/dist/cli/resume.js.map +1 -1
  15. package/dist/cli/run.js +28 -9
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/cli/signal.d.ts +6 -1
  18. package/dist/cli/signal.js +94 -12
  19. package/dist/cli/signal.js.map +1 -1
  20. package/dist/cli/tasks.js +3 -46
  21. package/dist/cli/tasks.js.map +1 -1
  22. package/dist/core/agent-supervisor.d.ts +23 -0
  23. package/dist/core/agent-supervisor.js +42 -0
  24. package/dist/core/agent-supervisor.js.map +1 -0
  25. package/dist/core/auto-recovery.d.ts +2 -1
  26. package/dist/core/auto-recovery.js +6 -1
  27. package/dist/core/auto-recovery.js.map +1 -1
  28. package/dist/core/failure-policy.d.ts +0 -1
  29. package/dist/core/failure-policy.js +0 -1
  30. package/dist/core/failure-policy.js.map +1 -1
  31. package/dist/core/git-lifecycle-manager.d.ts +284 -0
  32. package/dist/core/git-lifecycle-manager.js +778 -0
  33. package/dist/core/git-lifecycle-manager.js.map +1 -0
  34. package/dist/core/git-pipeline-coordinator.d.ts +21 -0
  35. package/dist/core/git-pipeline-coordinator.js +205 -0
  36. package/dist/core/git-pipeline-coordinator.js.map +1 -0
  37. package/dist/core/intervention.d.ts +176 -0
  38. package/dist/core/intervention.js +424 -0
  39. package/dist/core/intervention.js.map +1 -0
  40. package/dist/core/lane-state-machine.d.ts +423 -0
  41. package/dist/core/lane-state-machine.js +890 -0
  42. package/dist/core/lane-state-machine.js.map +1 -0
  43. package/dist/core/orchestrator.d.ts +4 -1
  44. package/dist/core/orchestrator.js +38 -63
  45. package/dist/core/orchestrator.js.map +1 -1
  46. package/dist/core/runner/agent.d.ts +7 -1
  47. package/dist/core/runner/agent.js +45 -30
  48. package/dist/core/runner/agent.js.map +1 -1
  49. package/dist/core/runner/pipeline.js +283 -109
  50. package/dist/core/runner/pipeline.js.map +1 -1
  51. package/dist/core/runner/task.d.ts +4 -5
  52. package/dist/core/runner/task.js +6 -77
  53. package/dist/core/runner/task.js.map +1 -1
  54. package/dist/core/runner.js +11 -2
  55. package/dist/core/runner.js.map +1 -1
  56. package/dist/core/stall-detection.d.ts +27 -4
  57. package/dist/core/stall-detection.js +116 -28
  58. package/dist/core/stall-detection.js.map +1 -1
  59. package/dist/hooks/contexts/index.d.ts +104 -0
  60. package/dist/hooks/contexts/index.js +134 -0
  61. package/dist/hooks/contexts/index.js.map +1 -0
  62. package/dist/hooks/data-accessor.d.ts +86 -0
  63. package/dist/hooks/data-accessor.js +410 -0
  64. package/dist/hooks/data-accessor.js.map +1 -0
  65. package/dist/hooks/flow-controller.d.ts +136 -0
  66. package/dist/hooks/flow-controller.js +351 -0
  67. package/dist/hooks/flow-controller.js.map +1 -0
  68. package/dist/hooks/index.d.ts +68 -0
  69. package/dist/hooks/index.js +105 -0
  70. package/dist/hooks/index.js.map +1 -0
  71. package/dist/hooks/manager.d.ts +129 -0
  72. package/dist/hooks/manager.js +389 -0
  73. package/dist/hooks/manager.js.map +1 -0
  74. package/dist/hooks/types.d.ts +463 -0
  75. package/dist/hooks/types.js +45 -0
  76. package/dist/hooks/types.js.map +1 -0
  77. package/dist/services/logging/buffer.d.ts +2 -2
  78. package/dist/services/logging/buffer.js +95 -42
  79. package/dist/services/logging/buffer.js.map +1 -1
  80. package/dist/services/logging/console.js +8 -5
  81. package/dist/services/logging/console.js.map +1 -1
  82. package/dist/services/logging/formatter.d.ts +9 -3
  83. package/dist/services/logging/formatter.js +64 -17
  84. package/dist/services/logging/formatter.js.map +1 -1
  85. package/dist/services/logging/index.d.ts +0 -1
  86. package/dist/services/logging/index.js +0 -1
  87. package/dist/services/logging/index.js.map +1 -1
  88. package/dist/services/logging/paths.d.ts +8 -0
  89. package/dist/services/logging/paths.js +48 -0
  90. package/dist/services/logging/paths.js.map +1 -0
  91. package/dist/services/logging/raw-log.d.ts +6 -0
  92. package/dist/services/logging/raw-log.js +37 -0
  93. package/dist/services/logging/raw-log.js.map +1 -0
  94. package/dist/services/process/index.js +1 -1
  95. package/dist/services/process/index.js.map +1 -1
  96. package/dist/types/agent.d.ts +15 -0
  97. package/dist/types/config.d.ts +24 -1
  98. package/dist/types/event-categories.d.ts +601 -0
  99. package/dist/types/event-categories.js +233 -0
  100. package/dist/types/event-categories.js.map +1 -0
  101. package/dist/types/events.d.ts +0 -20
  102. package/dist/types/flow.d.ts +10 -6
  103. package/dist/types/index.d.ts +1 -1
  104. package/dist/types/index.js +17 -3
  105. package/dist/types/index.js.map +1 -1
  106. package/dist/types/lane.d.ts +1 -1
  107. package/dist/types/logging.d.ts +1 -1
  108. package/dist/types/task.d.ts +13 -2
  109. package/dist/ui/log-viewer.d.ts +3 -0
  110. package/dist/ui/log-viewer.js +3 -0
  111. package/dist/ui/log-viewer.js.map +1 -1
  112. package/dist/utils/config.js +15 -1
  113. package/dist/utils/config.js.map +1 -1
  114. package/dist/utils/cursor-agent.d.ts +11 -1
  115. package/dist/utils/cursor-agent.js +63 -16
  116. package/dist/utils/cursor-agent.js.map +1 -1
  117. package/dist/utils/enhanced-logger.d.ts +5 -1
  118. package/dist/utils/enhanced-logger.js +99 -20
  119. package/dist/utils/enhanced-logger.js.map +1 -1
  120. package/dist/utils/event-registry.d.ts +222 -0
  121. package/dist/utils/event-registry.js +463 -0
  122. package/dist/utils/event-registry.js.map +1 -0
  123. package/dist/utils/events.d.ts +1 -13
  124. package/dist/utils/events.js.map +1 -1
  125. package/dist/utils/flow.d.ts +10 -0
  126. package/dist/utils/flow.js +75 -0
  127. package/dist/utils/flow.js.map +1 -1
  128. package/dist/utils/git.d.ts +12 -1
  129. package/dist/utils/git.js +54 -1
  130. package/dist/utils/git.js.map +1 -1
  131. package/dist/utils/log-constants.d.ts +1 -0
  132. package/dist/utils/log-constants.js +2 -1
  133. package/dist/utils/log-constants.js.map +1 -1
  134. package/dist/utils/log-formatter.d.ts +3 -2
  135. package/dist/utils/log-formatter.js +11 -11
  136. package/dist/utils/log-formatter.js.map +1 -1
  137. package/dist/utils/logger.d.ts +11 -0
  138. package/dist/utils/logger.js +82 -3
  139. package/dist/utils/logger.js.map +1 -1
  140. package/dist/utils/repro-thinking-logs.js +0 -13
  141. package/dist/utils/repro-thinking-logs.js.map +1 -1
  142. package/dist/utils/run-service.js +1 -1
  143. package/dist/utils/run-service.js.map +1 -1
  144. package/examples/README.md +0 -2
  145. package/examples/demo-project/README.md +1 -2
  146. package/package.json +18 -28
  147. package/scripts/setup-security.sh +0 -1
  148. package/scripts/test-log-parser.ts +171 -0
  149. package/scripts/verify-change.sh +272 -0
  150. package/src/cli/logs.ts +121 -10
  151. package/src/cli/models.ts +20 -3
  152. package/src/cli/monitor.ts +1257 -1342
  153. package/src/cli/prepare.ts +0 -1
  154. package/src/cli/resume.ts +29 -5
  155. package/src/cli/run.ts +29 -11
  156. package/src/cli/signal.ts +115 -17
  157. package/src/cli/tasks.ts +2 -59
  158. package/src/core/agent-supervisor.ts +64 -0
  159. package/src/core/auto-recovery.ts +7 -1
  160. package/src/core/failure-policy.ts +0 -1
  161. package/src/core/git-lifecycle-manager.ts +1011 -0
  162. package/src/core/git-pipeline-coordinator.ts +221 -0
  163. package/src/core/intervention.ts +481 -0
  164. package/src/core/lane-state-machine.ts +1097 -0
  165. package/src/core/orchestrator.ts +45 -62
  166. package/src/core/runner/agent.ts +66 -33
  167. package/src/core/runner/pipeline.ts +318 -122
  168. package/src/core/runner/task.ts +12 -93
  169. package/src/core/runner.ts +12 -2
  170. package/src/core/stall-detection.ts +145 -28
  171. package/src/hooks/contexts/index.ts +256 -0
  172. package/src/hooks/data-accessor.ts +488 -0
  173. package/src/hooks/flow-controller.ts +425 -0
  174. package/src/hooks/index.ts +154 -0
  175. package/src/hooks/manager.ts +434 -0
  176. package/src/hooks/types.ts +544 -0
  177. package/src/services/logging/buffer.ts +104 -43
  178. package/src/services/logging/console.ts +9 -5
  179. package/src/services/logging/formatter.ts +74 -17
  180. package/src/services/logging/index.ts +0 -2
  181. package/src/services/logging/paths.ts +14 -0
  182. package/src/services/logging/raw-log.ts +43 -0
  183. package/src/services/process/index.ts +1 -1
  184. package/src/types/agent.ts +15 -0
  185. package/src/types/config.ts +25 -1
  186. package/src/types/event-categories.ts +663 -0
  187. package/src/types/events.ts +0 -25
  188. package/src/types/flow.ts +10 -6
  189. package/src/types/index.ts +50 -4
  190. package/src/types/lane.ts +1 -2
  191. package/src/types/logging.ts +2 -1
  192. package/src/types/task.ts +13 -2
  193. package/src/ui/log-viewer.ts +3 -0
  194. package/src/utils/config.ts +17 -1
  195. package/src/utils/cursor-agent.ts +68 -16
  196. package/src/utils/enhanced-logger.ts +106 -20
  197. package/src/utils/event-registry.ts +595 -0
  198. package/src/utils/events.ts +0 -16
  199. package/src/utils/flow.ts +84 -0
  200. package/src/utils/git.ts +59 -1
  201. package/src/utils/log-constants.ts +2 -1
  202. package/src/utils/log-formatter.ts +11 -12
  203. package/src/utils/logger.ts +49 -3
  204. package/src/utils/repro-thinking-logs.ts +0 -15
  205. package/src/utils/run-service.ts +1 -1
  206. package/dist/services/logging/file-writer.d.ts +0 -71
  207. package/dist/services/logging/file-writer.js +0 -516
  208. package/dist/services/logging/file-writer.js.map +0 -1
  209. package/dist/types/review.d.ts +0 -17
  210. package/dist/types/review.js +0 -6
  211. package/dist/types/review.js.map +0 -1
  212. package/scripts/ai-security-check.js +0 -233
  213. package/src/services/logging/file-writer.ts +0 -526
  214. package/src/types/review.ts +0 -20
@@ -13,14 +13,30 @@ import {
13
13
  TaskExecutionResult,
14
14
  LaneState
15
15
  } from '../../types';
16
- import {
17
- cursorAgentCreateChat
18
- } from './agent';
16
+ import { AgentSupervisor } from '../agent-supervisor';
19
17
  import {
20
18
  runTask,
21
- waitForTaskDependencies,
22
- mergeDependencyBranches
19
+ waitForTaskDependencies
23
20
  } from './task';
21
+ import { GitPipelineCoordinator } from '../git-pipeline-coordinator';
22
+ import {
23
+ readPendingIntervention,
24
+ clearPendingIntervention,
25
+ InterventionRequest,
26
+ } from '../intervention';
27
+ import {
28
+ getHookManager,
29
+ HookPoint,
30
+ createBeforeTaskContext,
31
+ createAfterTaskContext,
32
+ createOnErrorContext,
33
+ createOnLaneEndContext,
34
+ FlowAbortError,
35
+ FlowRetryError,
36
+ TaskDefinition,
37
+ TaskResult as HookTaskResult,
38
+ DependencyResult,
39
+ } from '../../hooks';
24
40
 
25
41
  /**
26
42
  * Validate task configuration
@@ -37,6 +53,26 @@ function validateTaskConfig(config: RunnerConfig): void {
37
53
  }
38
54
  }
39
55
 
56
+ /**
57
+ * Check for pending intervention and return intervention message if present
58
+ * This is called at the start of each task to inject intervention messages
59
+ */
60
+ function checkAndConsumePendingIntervention(runDir: string): InterventionRequest | null {
61
+ const intervention = readPendingIntervention(runDir);
62
+
63
+ if (intervention) {
64
+ logger.info(`📨 Pending intervention found (type: ${intervention.type})`);
65
+ logger.info(` Message: "${intervention.message.substring(0, 80)}${intervention.message.length > 80 ? '...' : ''}"`);
66
+
67
+ // Clear the intervention file so it's not picked up again
68
+ clearPendingIntervention(runDir);
69
+
70
+ return intervention;
71
+ }
72
+
73
+ return null;
74
+ }
75
+
40
76
  /**
41
77
  * Run all tasks in sequence
42
78
  */
@@ -175,51 +211,13 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
175
211
  logger.info(`Worktree: ${worktreeDir}`);
176
212
  logger.info(`Tasks: ${config.tasks.length}`);
177
213
 
178
- // Create worktree only if starting fresh and worktree doesn't exist
179
- if (!fs.existsSync(worktreeDir)) {
180
- // Use a simple retry mechanism for Git worktree creation to handle potential race conditions
181
- let retries = 3;
182
- let lastError: Error | null = null;
183
-
184
- while (retries > 0) {
185
- try {
186
- // Ensure parent directory exists before calling git worktree
187
- const worktreeParent = path.dirname(worktreeDir);
188
- if (!fs.existsSync(worktreeParent)) {
189
- fs.mkdirSync(worktreeParent, { recursive: true });
190
- }
191
-
192
- // Always use the current branch (already captured at start) as the base branch
193
- await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
194
- baseBranch: currentBranch,
195
- cwd: repoRoot,
196
- });
197
- break; // Success
198
- } catch (e: any) {
199
- lastError = e;
200
- retries--;
201
- if (retries > 0) {
202
- const delay = Math.floor(Math.random() * 1000) + 500;
203
- logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
204
- await new Promise(resolve => setTimeout(resolve, delay));
205
- }
206
- }
207
- }
208
-
209
- if (retries === 0 && lastError) {
210
- throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
211
- }
212
- } else {
213
- // If it exists, ensure it's actually a worktree and on the right branch
214
- logger.info(`Reusing existing worktree: ${worktreeDir}`);
215
- try {
216
- git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
217
- } catch (e) {
218
- // If checkout fails, maybe the worktree is in a weird state.
219
- // For now, just log it. In a more robust impl, we might want to repair it.
220
- logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
221
- }
222
- }
214
+ const gitCoordinator = new GitPipelineCoordinator();
215
+ await gitCoordinator.ensureWorktree({
216
+ worktreeDir,
217
+ pipelineBranch,
218
+ repoRoot,
219
+ baseBranch: currentBranch,
220
+ });
223
221
 
224
222
  // Change current directory to worktree for all subsequent operations
225
223
  // This ensures that all spawned processes (like git or npm) inherit the correct CWD
@@ -228,7 +226,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
228
226
 
229
227
  // Create chat
230
228
  logger.info('Creating chat session...');
231
- const chatId = cursorAgentCreateChat(worktreeDir);
229
+ const agentSupervisor = new AgentSupervisor();
230
+ const chatId = agentSupervisor.createChat(worktreeDir);
232
231
 
233
232
  // Initialize state if not loaded
234
233
  if (!state) {
@@ -264,6 +263,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
264
263
  let previousTaskBranch: string | null = null;
265
264
 
266
265
  for (let i = startIndex; i < config.tasks.length; i++) {
266
+ // Check for pending intervention at the start of each task
267
+ // This handles both resume cases and mid-run interventions
268
+ const intervention = checkAndConsumePendingIntervention(runDir);
269
+
267
270
  // Re-read tasks file to allow dynamic updates to future tasks
268
271
  try {
269
272
  const currentConfig = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
@@ -291,7 +294,15 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
291
294
  logger.warn(`⚠️ Could not reload tasks from ${tasksFile}. Using existing configuration. (${e instanceof Error ? e.message : String(e)})`);
292
295
  }
293
296
 
294
- const task = config.tasks[i]!;
297
+ // Clone the task to avoid mutating the original config
298
+ let task = { ...config.tasks[i]! };
299
+
300
+ // If there's a pending intervention, prepend it to the task prompt
301
+ if (intervention) {
302
+ const originalPrompt = task.prompt;
303
+ task.prompt = `${intervention.message}\n\n---\n\nContinue with the following task:\n${originalPrompt}`;
304
+ logger.info(`🔀 Intervention message injected into task prompt`);
305
+ }
295
306
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
296
307
 
297
308
  // Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
@@ -331,7 +342,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
331
342
  onTimeout: 'fail',
332
343
  });
333
344
 
334
- await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
345
+ await gitCoordinator.mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
335
346
 
336
347
  state.status = 'running';
337
348
  state.waitingFor = [];
@@ -354,8 +365,103 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
354
365
  }
355
366
  }
356
367
 
368
+ // =========================================================================
369
+ // Hook System: Prepare context options
370
+ // =========================================================================
371
+ const hookManager = getHookManager();
372
+ const taskStartTime = Date.now();
373
+
374
+ // Build completed tasks list for hooks
375
+ const completedTasksForHooks: HookTaskResult[] = (state.completedTasks || []).map((name, idx) => ({
376
+ name,
377
+ status: 'success' as const,
378
+ duration: 0, // Not tracked historically
379
+ }));
380
+
381
+ // Convert config.tasks to TaskDefinition format
382
+ const tasksAsDefinitions: TaskDefinition[] = config.tasks.map(t => ({
383
+ name: t.name,
384
+ prompt: t.prompt,
385
+ model: t.model,
386
+ timeout: t.timeout,
387
+ dependsOn: t.dependsOn,
388
+ }));
389
+
390
+ const hookContextOptions = {
391
+ laneName,
392
+ runId: path.basename(path.dirname(runDir)),
393
+ taskIndex: i,
394
+ totalTasks: config.tasks.length,
395
+ task: {
396
+ name: task.name,
397
+ prompt: task.prompt,
398
+ model: task.model || config.model || 'sonnet-4.5',
399
+ dependsOn: task.dependsOn,
400
+ },
401
+ worktreeDir,
402
+ runDir,
403
+ taskBranch,
404
+ pipelineBranch,
405
+ tasksFile,
406
+ chatId,
407
+ tasks: tasksAsDefinitions,
408
+ completedTasks: completedTasksForHooks,
409
+ dependencyResults: [] as DependencyResult[], // TODO: populate from actual deps
410
+ taskStartTime,
411
+ laneStartTime: state.startTime || Date.now(),
412
+ agentSupervisor,
413
+ };
414
+
415
+ // =========================================================================
416
+ // Hook: beforeTask
417
+ // =========================================================================
418
+ let currentTask = task;
419
+
420
+ if (hookManager.hasHooks(HookPoint.BEFORE_TASK)) {
421
+ try {
422
+ const { context, flowController } = createBeforeTaskContext(hookContextOptions);
423
+ await hookManager.executeBeforeTask(context);
424
+
425
+ // Check if prompt was modified
426
+ const modifiedPrompt = flowController.getModifiedPrompt();
427
+ if (modifiedPrompt) {
428
+ currentTask = { ...task, prompt: modifiedPrompt };
429
+ logger.info(`🔧 [Hook] Task prompt modified by beforeTask hook`);
430
+ }
431
+
432
+ // Re-read tasks in case hooks modified them
433
+ hookContextOptions.tasks = config.tasks.map(t => ({
434
+ name: t.name,
435
+ prompt: t.prompt,
436
+ model: t.model,
437
+ timeout: t.timeout,
438
+ dependsOn: t.dependsOn,
439
+ }));
440
+ } catch (hookError: any) {
441
+ if (hookError instanceof FlowAbortError) {
442
+ state.status = 'failed';
443
+ state.error = hookError.message;
444
+ saveState(statePath, state);
445
+ logger.error(`[Hook] Flow aborted: ${hookError.message}`);
446
+ process.exit(1);
447
+ }
448
+ if (hookError instanceof FlowRetryError) {
449
+ // Retry with modified prompt if provided
450
+ if (hookError.modifiedPrompt) {
451
+ currentTask = { ...task, prompt: hookError.modifiedPrompt };
452
+ }
453
+ logger.info(`[Hook] Retry requested, continuing with ${hookError.modifiedPrompt ? 'modified' : 'original'} prompt`);
454
+ } else {
455
+ logger.warn(`[Hook] beforeTask hook error: ${hookError.message}`);
456
+ }
457
+ }
458
+ }
459
+
460
+ // =========================================================================
461
+ // Execute Task
462
+ // =========================================================================
357
463
  const result = await runTask({
358
- task,
464
+ task: currentTask,
359
465
  config,
360
466
  index: i,
361
467
  worktreeDir,
@@ -363,6 +469,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
363
469
  taskBranch,
364
470
  chatId,
365
471
  runDir,
472
+ agentSupervisor,
473
+ laneName,
366
474
  });
367
475
 
368
476
  results.push(result);
@@ -375,8 +483,74 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
375
483
  }
376
484
  saveState(statePath, state);
377
485
 
486
+ // =========================================================================
487
+ // Hook: afterTask
488
+ // =========================================================================
489
+ if (hookManager.hasHooks(HookPoint.AFTER_TASK)) {
490
+ try {
491
+ // Update completed tasks for the context
492
+ hookContextOptions.completedTasks = (state.completedTasks || []).map((name) => ({
493
+ name,
494
+ status: 'success' as const,
495
+ duration: Date.now() - taskStartTime,
496
+ }));
497
+
498
+ const afterTaskResult = {
499
+ status: (result.status === 'FINISHED' ? 'success' :
500
+ result.status === 'BLOCKED_DEPENDENCY' ? 'blocked' : 'error') as 'success' | 'error' | 'blocked',
501
+ exitCode: result.status === 'FINISHED' ? 0 : 1,
502
+ error: result.error,
503
+ };
504
+
505
+ const { context } = createAfterTaskContext(hookContextOptions, afterTaskResult);
506
+ await hookManager.executeAfterTask(context);
507
+
508
+ // Re-read tasks in case hooks modified them (injected new tasks)
509
+ try {
510
+ const updatedConfig = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
511
+ if (updatedConfig.tasks && updatedConfig.tasks.length !== config.tasks.length) {
512
+ config.tasks = updatedConfig.tasks;
513
+ state.totalTasks = config.tasks.length;
514
+ saveState(statePath, state);
515
+ logger.info(`📋 [Hook] Task list updated by afterTask hook. New total: ${state.totalTasks}`);
516
+ }
517
+ } catch {
518
+ // Ignore file read errors
519
+ }
520
+ } catch (hookError: any) {
521
+ if (hookError instanceof FlowAbortError) {
522
+ state.status = 'failed';
523
+ state.error = hookError.message;
524
+ saveState(statePath, state);
525
+ logger.error(`[Hook] Flow aborted: ${hookError.message}`);
526
+ process.exit(1);
527
+ }
528
+ if (hookError instanceof FlowRetryError) {
529
+ // Decrement index to retry this task
530
+ i--;
531
+ logger.info(`[Hook] Retry requested for task "${task.name}"`);
532
+ continue;
533
+ }
534
+ logger.warn(`[Hook] afterTask hook error: ${hookError.message}`);
535
+ }
536
+ }
537
+
378
538
  // Handle blocked or error
379
539
  if (result.status === 'BLOCKED_DEPENDENCY') {
540
+ // Execute onError hook for blocked state
541
+ if (hookManager.hasHooks(HookPoint.ON_ERROR)) {
542
+ try {
543
+ const { context } = createOnErrorContext(hookContextOptions, {
544
+ type: 'unknown',
545
+ message: 'Task blocked on dependency change',
546
+ retryable: false,
547
+ });
548
+ await hookManager.executeOnError(context);
549
+ } catch {
550
+ // Continue with normal error handling
551
+ }
552
+ }
553
+
380
554
  state.status = 'failed';
381
555
  state.dependencyRequest = result.dependencyRequest || null;
382
556
  saveState(statePath, state);
@@ -393,6 +567,24 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
393
567
  }
394
568
 
395
569
  if (result.status !== 'FINISHED') {
570
+ // Execute onError hook
571
+ if (hookManager.hasHooks(HookPoint.ON_ERROR)) {
572
+ try {
573
+ const { context } = createOnErrorContext(hookContextOptions, {
574
+ type: 'agent_error',
575
+ message: result.error || 'Unknown error',
576
+ retryable: true,
577
+ });
578
+ await hookManager.executeOnError(context);
579
+ } catch (hookError: any) {
580
+ if (hookError instanceof FlowRetryError) {
581
+ i--;
582
+ logger.info(`[Hook] Retry requested after error`);
583
+ continue;
584
+ }
585
+ }
586
+ }
587
+
396
588
  state.status = 'failed';
397
589
  state.error = result.error || 'Unknown error';
398
590
  saveState(statePath, state);
@@ -401,52 +593,18 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
401
593
  }
402
594
 
403
595
  // Merge into pipeline
404
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
405
-
406
- // Ensure we are on the pipeline branch before merging the task branch
407
- logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
408
- git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
409
-
410
- // Pre-check for conflicts (should be rare since task branch was created from pipeline)
411
- const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
412
- if (conflictCheck.willConflict) {
413
- logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
414
- logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
415
- logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
416
-
417
- events.emit('merge.conflict_detected', {
596
+ try {
597
+ gitCoordinator.mergeTaskIntoPipeline({
418
598
  taskName: task.name,
419
599
  taskBranch,
420
600
  pipelineBranch,
421
- conflictingFiles: conflictCheck.conflictingFiles,
422
- preCheck: true,
601
+ worktreeDir,
423
602
  });
424
- }
425
-
426
- // Use safeMerge instead of plain merge for better error handling
427
- logger.info(`🔀 Merging task ${task.name} (${taskBranch}) into ${pipelineBranch}`);
428
- const mergeResult = git.safeMerge(taskBranch, {
429
- cwd: worktreeDir,
430
- noFf: true,
431
- message: `chore: merge task ${task.name} into pipeline`,
432
- abortOnConflict: true,
433
- });
434
-
435
- if (!mergeResult.success) {
436
- if (mergeResult.conflict) {
437
- logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
438
- state.status = 'failed';
439
- state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
440
- saveState(statePath, state);
441
- process.exit(1);
442
- }
443
- throw new Error(mergeResult.error || 'Merge failed');
444
- }
445
-
446
- // Log changed files
447
- const stats = git.getLastOperationStats(worktreeDir);
448
- if (stats) {
449
- logger.info('Changed files:\n' + stats);
603
+ } catch (e: any) {
604
+ state.status = 'failed';
605
+ state.error = e.message;
606
+ saveState(statePath, state);
607
+ process.exit(1);
450
608
  }
451
609
 
452
610
  git.push(pipelineBranch, { cwd: worktreeDir });
@@ -470,31 +628,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
470
628
  }
471
629
 
472
630
  // 2. Create flow branch from pipelineBranch and cleanup
473
- if (flowBranch !== pipelineBranch) {
474
- logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
475
- try {
476
- // Create/Overwrite flow branch from pipeline branch
477
- git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
478
- git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
479
-
480
- // 3. Delete temporary pipeline branch
481
- logger.info(`🗑️ Deleting temporary pipeline branch: ${pipelineBranch}`);
482
- // Must be on another branch to delete pipelineBranch
483
- git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
484
- git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
485
-
486
- try {
487
- git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true, remote: true });
488
- logger.info(` Deleted remote branch: origin/${pipelineBranch}`);
489
- } catch {
490
- // May not exist on remote or delete failed
491
- }
492
-
493
- logger.success(`✓ Flow branch '${flowBranch}' is now the only remaining branch.`);
494
- } catch (e) {
495
- logger.error(`❌ Failed during final consolidation: ${e}`);
496
- }
497
- }
631
+ gitCoordinator.finalizeFlowBranch({
632
+ flowBranch,
633
+ pipelineBranch,
634
+ worktreeDir,
635
+ });
498
636
 
499
637
  // Complete
500
638
  state.status = 'completed';
@@ -512,7 +650,65 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
512
650
  // Ignore
513
651
  }
514
652
 
653
+ // =========================================================================
654
+ // Hook: onLaneEnd
655
+ // =========================================================================
656
+ const hookManager = getHookManager();
657
+ if (hookManager.hasHooks(HookPoint.ON_LANE_END)) {
658
+ try {
659
+ const totalDuration = (state.endTime || Date.now()) - (state.startTime || Date.now());
660
+ const failedCount = results.filter(r => r.status !== 'FINISHED').length;
661
+
662
+ const laneEndContextOptions = {
663
+ laneName,
664
+ runId: path.basename(path.dirname(runDir)),
665
+ taskIndex: config.tasks.length - 1,
666
+ totalTasks: config.tasks.length,
667
+ task: {
668
+ name: config.tasks[config.tasks.length - 1]?.name || 'unknown',
669
+ prompt: '',
670
+ model: config.model || 'sonnet-4.5',
671
+ },
672
+ worktreeDir,
673
+ runDir,
674
+ taskBranch: pipelineBranch,
675
+ pipelineBranch,
676
+ tasksFile,
677
+ chatId,
678
+ tasks: config.tasks.map(t => ({
679
+ name: t.name,
680
+ prompt: t.prompt,
681
+ model: t.model,
682
+ timeout: t.timeout,
683
+ dependsOn: t.dependsOn,
684
+ })),
685
+ completedTasks: (state.completedTasks || []).map(name => ({
686
+ name,
687
+ status: 'success' as const,
688
+ duration: 0,
689
+ })),
690
+ dependencyResults: [] as DependencyResult[],
691
+ taskStartTime: state.startTime || Date.now(),
692
+ laneStartTime: state.startTime || Date.now(),
693
+ agentSupervisor,
694
+ };
695
+
696
+ const { context } = createOnLaneEndContext(laneEndContextOptions, {
697
+ status: 'completed',
698
+ completedTasks: results.length - failedCount,
699
+ failedTasks: failedCount,
700
+ totalDuration,
701
+ });
702
+
703
+ // Execute async (don't block lane completion)
704
+ hookManager.executeOnLaneEnd(context).catch(err => {
705
+ logger.warn(`[Hook] onLaneEnd error: ${err.message}`);
706
+ });
707
+ } catch (hookError: any) {
708
+ logger.warn(`[Hook] onLaneEnd setup error: ${hookError.message}`);
709
+ }
710
+ }
711
+
515
712
  logger.success('All tasks completed!');
516
713
  return results;
517
714
  }
518
-