@litmers/cursorflow-orchestrator 0.1.40 → 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 (207) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/README.md +7 -3
  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/resume.js +21 -1
  12. package/dist/cli/resume.js.map +1 -1
  13. package/dist/cli/run.js +28 -9
  14. package/dist/cli/run.js.map +1 -1
  15. package/dist/cli/signal.d.ts +6 -1
  16. package/dist/cli/signal.js +94 -12
  17. package/dist/cli/signal.js.map +1 -1
  18. package/dist/cli/tasks.js +3 -46
  19. package/dist/cli/tasks.js.map +1 -1
  20. package/dist/core/agent-supervisor.d.ts +23 -0
  21. package/dist/core/agent-supervisor.js +42 -0
  22. package/dist/core/agent-supervisor.js.map +1 -0
  23. package/dist/core/auto-recovery.d.ts +2 -1
  24. package/dist/core/auto-recovery.js +6 -1
  25. package/dist/core/auto-recovery.js.map +1 -1
  26. package/dist/core/failure-policy.d.ts +0 -1
  27. package/dist/core/failure-policy.js +0 -1
  28. package/dist/core/failure-policy.js.map +1 -1
  29. package/dist/core/git-lifecycle-manager.d.ts +284 -0
  30. package/dist/core/git-lifecycle-manager.js +778 -0
  31. package/dist/core/git-lifecycle-manager.js.map +1 -0
  32. package/dist/core/git-pipeline-coordinator.d.ts +21 -0
  33. package/dist/core/git-pipeline-coordinator.js +205 -0
  34. package/dist/core/git-pipeline-coordinator.js.map +1 -0
  35. package/dist/core/intervention.d.ts +176 -0
  36. package/dist/core/intervention.js +424 -0
  37. package/dist/core/intervention.js.map +1 -0
  38. package/dist/core/lane-state-machine.d.ts +423 -0
  39. package/dist/core/lane-state-machine.js +890 -0
  40. package/dist/core/lane-state-machine.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +4 -1
  42. package/dist/core/orchestrator.js +29 -62
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +7 -1
  45. package/dist/core/runner/agent.js +45 -30
  46. package/dist/core/runner/agent.js.map +1 -1
  47. package/dist/core/runner/pipeline.js +283 -123
  48. package/dist/core/runner/pipeline.js.map +1 -1
  49. package/dist/core/runner/task.d.ts +4 -5
  50. package/dist/core/runner/task.js +6 -80
  51. package/dist/core/runner/task.js.map +1 -1
  52. package/dist/core/runner.js +8 -2
  53. package/dist/core/runner.js.map +1 -1
  54. package/dist/core/stall-detection.d.ts +11 -4
  55. package/dist/core/stall-detection.js +62 -27
  56. package/dist/core/stall-detection.js.map +1 -1
  57. package/dist/hooks/contexts/index.d.ts +104 -0
  58. package/dist/hooks/contexts/index.js +134 -0
  59. package/dist/hooks/contexts/index.js.map +1 -0
  60. package/dist/hooks/data-accessor.d.ts +86 -0
  61. package/dist/hooks/data-accessor.js +410 -0
  62. package/dist/hooks/data-accessor.js.map +1 -0
  63. package/dist/hooks/flow-controller.d.ts +136 -0
  64. package/dist/hooks/flow-controller.js +351 -0
  65. package/dist/hooks/flow-controller.js.map +1 -0
  66. package/dist/hooks/index.d.ts +68 -0
  67. package/dist/hooks/index.js +105 -0
  68. package/dist/hooks/index.js.map +1 -0
  69. package/dist/hooks/manager.d.ts +129 -0
  70. package/dist/hooks/manager.js +389 -0
  71. package/dist/hooks/manager.js.map +1 -0
  72. package/dist/hooks/types.d.ts +463 -0
  73. package/dist/hooks/types.js +45 -0
  74. package/dist/hooks/types.js.map +1 -0
  75. package/dist/services/logging/buffer.d.ts +2 -2
  76. package/dist/services/logging/buffer.js +95 -42
  77. package/dist/services/logging/buffer.js.map +1 -1
  78. package/dist/services/logging/console.js +6 -1
  79. package/dist/services/logging/console.js.map +1 -1
  80. package/dist/services/logging/formatter.d.ts +9 -4
  81. package/dist/services/logging/formatter.js +64 -18
  82. package/dist/services/logging/formatter.js.map +1 -1
  83. package/dist/services/logging/index.d.ts +0 -1
  84. package/dist/services/logging/index.js +0 -1
  85. package/dist/services/logging/index.js.map +1 -1
  86. package/dist/services/logging/paths.d.ts +8 -0
  87. package/dist/services/logging/paths.js +48 -0
  88. package/dist/services/logging/paths.js.map +1 -0
  89. package/dist/services/logging/raw-log.d.ts +6 -0
  90. package/dist/services/logging/raw-log.js +37 -0
  91. package/dist/services/logging/raw-log.js.map +1 -0
  92. package/dist/services/process/index.js +1 -1
  93. package/dist/services/process/index.js.map +1 -1
  94. package/dist/types/agent.d.ts +15 -0
  95. package/dist/types/config.d.ts +22 -1
  96. package/dist/types/event-categories.d.ts +601 -0
  97. package/dist/types/event-categories.js +233 -0
  98. package/dist/types/event-categories.js.map +1 -0
  99. package/dist/types/events.d.ts +0 -20
  100. package/dist/types/flow.d.ts +10 -6
  101. package/dist/types/index.d.ts +1 -1
  102. package/dist/types/index.js +17 -3
  103. package/dist/types/index.js.map +1 -1
  104. package/dist/types/lane.d.ts +1 -1
  105. package/dist/types/logging.d.ts +1 -1
  106. package/dist/types/task.d.ts +12 -1
  107. package/dist/ui/log-viewer.d.ts +3 -0
  108. package/dist/ui/log-viewer.js +3 -0
  109. package/dist/ui/log-viewer.js.map +1 -1
  110. package/dist/utils/config.js +10 -1
  111. package/dist/utils/config.js.map +1 -1
  112. package/dist/utils/cursor-agent.d.ts +11 -1
  113. package/dist/utils/cursor-agent.js +63 -16
  114. package/dist/utils/cursor-agent.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +5 -1
  116. package/dist/utils/enhanced-logger.js +98 -19
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/event-registry.d.ts +222 -0
  119. package/dist/utils/event-registry.js +463 -0
  120. package/dist/utils/event-registry.js.map +1 -0
  121. package/dist/utils/events.d.ts +1 -13
  122. package/dist/utils/events.js.map +1 -1
  123. package/dist/utils/flow.d.ts +10 -0
  124. package/dist/utils/flow.js +75 -0
  125. package/dist/utils/flow.js.map +1 -1
  126. package/dist/utils/log-constants.d.ts +1 -0
  127. package/dist/utils/log-constants.js +2 -1
  128. package/dist/utils/log-constants.js.map +1 -1
  129. package/dist/utils/log-formatter.d.ts +2 -1
  130. package/dist/utils/log-formatter.js +10 -10
  131. package/dist/utils/log-formatter.js.map +1 -1
  132. package/dist/utils/logger.d.ts +11 -0
  133. package/dist/utils/logger.js +82 -3
  134. package/dist/utils/logger.js.map +1 -1
  135. package/dist/utils/repro-thinking-logs.js +0 -13
  136. package/dist/utils/repro-thinking-logs.js.map +1 -1
  137. package/dist/utils/run-service.js +1 -1
  138. package/dist/utils/run-service.js.map +1 -1
  139. package/examples/README.md +0 -2
  140. package/examples/demo-project/README.md +1 -2
  141. package/package.json +18 -28
  142. package/scripts/setup-security.sh +0 -1
  143. package/scripts/test-log-parser.ts +171 -0
  144. package/scripts/verify-change.sh +272 -0
  145. package/src/cli/logs.ts +121 -10
  146. package/src/cli/models.ts +20 -3
  147. package/src/cli/monitor.ts +1257 -1342
  148. package/src/cli/resume.ts +27 -1
  149. package/src/cli/run.ts +29 -11
  150. package/src/cli/signal.ts +115 -17
  151. package/src/cli/tasks.ts +2 -59
  152. package/src/core/agent-supervisor.ts +64 -0
  153. package/src/core/auto-recovery.ts +7 -1
  154. package/src/core/failure-policy.ts +0 -1
  155. package/src/core/git-lifecycle-manager.ts +1011 -0
  156. package/src/core/git-pipeline-coordinator.ts +221 -0
  157. package/src/core/intervention.ts +481 -0
  158. package/src/core/lane-state-machine.ts +1097 -0
  159. package/src/core/orchestrator.ts +35 -61
  160. package/src/core/runner/agent.ts +66 -33
  161. package/src/core/runner/pipeline.ts +318 -138
  162. package/src/core/runner/task.ts +12 -97
  163. package/src/core/runner.ts +8 -2
  164. package/src/core/stall-detection.ts +72 -27
  165. package/src/hooks/contexts/index.ts +256 -0
  166. package/src/hooks/data-accessor.ts +488 -0
  167. package/src/hooks/flow-controller.ts +425 -0
  168. package/src/hooks/index.ts +154 -0
  169. package/src/hooks/manager.ts +434 -0
  170. package/src/hooks/types.ts +544 -0
  171. package/src/services/logging/buffer.ts +104 -43
  172. package/src/services/logging/console.ts +7 -1
  173. package/src/services/logging/formatter.ts +74 -18
  174. package/src/services/logging/index.ts +0 -2
  175. package/src/services/logging/paths.ts +14 -0
  176. package/src/services/logging/raw-log.ts +43 -0
  177. package/src/services/process/index.ts +1 -1
  178. package/src/types/agent.ts +15 -0
  179. package/src/types/config.ts +23 -1
  180. package/src/types/event-categories.ts +663 -0
  181. package/src/types/events.ts +0 -25
  182. package/src/types/flow.ts +10 -6
  183. package/src/types/index.ts +50 -4
  184. package/src/types/lane.ts +1 -2
  185. package/src/types/logging.ts +2 -1
  186. package/src/types/task.ts +12 -1
  187. package/src/ui/log-viewer.ts +3 -0
  188. package/src/utils/config.ts +11 -1
  189. package/src/utils/cursor-agent.ts +68 -16
  190. package/src/utils/enhanced-logger.ts +105 -19
  191. package/src/utils/event-registry.ts +595 -0
  192. package/src/utils/events.ts +0 -16
  193. package/src/utils/flow.ts +83 -0
  194. package/src/utils/log-constants.ts +2 -1
  195. package/src/utils/log-formatter.ts +10 -11
  196. package/src/utils/logger.ts +49 -3
  197. package/src/utils/repro-thinking-logs.ts +0 -15
  198. package/src/utils/run-service.ts +1 -1
  199. package/dist/services/logging/file-writer.d.ts +0 -71
  200. package/dist/services/logging/file-writer.js +0 -516
  201. package/dist/services/logging/file-writer.js.map +0 -1
  202. package/dist/types/review.d.ts +0 -17
  203. package/dist/types/review.js +0 -6
  204. package/dist/types/review.js.map +0 -1
  205. package/scripts/ai-security-check.js +0 -233
  206. package/src/services/logging/file-writer.ts +0 -526
  207. 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,68 +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
- // Check if worktree needs to be created or repaired
179
- const worktreeNeedsCreation = !fs.existsSync(worktreeDir);
180
- const worktreeIsInvalid = !worktreeNeedsCreation && !git.isValidWorktree(worktreeDir);
181
-
182
- if (worktreeIsInvalid) {
183
- // Directory exists but is NOT a valid worktree - this can cause branch leakage!
184
- // Clean it up and recreate
185
- logger.warn(`⚠️ Directory exists but is not a valid worktree: ${worktreeDir}`);
186
- logger.info(` Cleaning up invalid directory and recreating worktree...`);
187
- try {
188
- git.cleanupInvalidWorktreeDir(worktreeDir);
189
- } catch (e: any) {
190
- logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`);
191
- throw new Error(`Cannot proceed: worktree directory is invalid and cleanup failed`);
192
- }
193
- }
194
-
195
- // Create worktree if it doesn't exist or was just cleaned up
196
- if (worktreeNeedsCreation || worktreeIsInvalid) {
197
- // Use a simple retry mechanism for Git worktree creation to handle potential race conditions
198
- let retries = 3;
199
- let lastError: Error | null = null;
200
-
201
- while (retries > 0) {
202
- try {
203
- // Ensure parent directory exists before calling git worktree
204
- const worktreeParent = path.dirname(worktreeDir);
205
- if (!fs.existsSync(worktreeParent)) {
206
- fs.mkdirSync(worktreeParent, { recursive: true });
207
- }
208
-
209
- // Always use the current branch (already captured at start) as the base branch
210
- await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
211
- baseBranch: currentBranch,
212
- cwd: repoRoot,
213
- });
214
- break; // Success
215
- } catch (e: any) {
216
- lastError = e;
217
- retries--;
218
- if (retries > 0) {
219
- const delay = Math.floor(Math.random() * 1000) + 500;
220
- logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
221
- await new Promise(resolve => setTimeout(resolve, delay));
222
- }
223
- }
224
- }
225
-
226
- if (retries === 0 && lastError) {
227
- throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
228
- }
229
- } else {
230
- // Worktree exists and is valid - reuse it
231
- logger.info(`Reusing existing worktree: ${worktreeDir}`);
232
- try {
233
- git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
234
- } catch (e) {
235
- // If checkout fails in a valid worktree, log warning but continue
236
- // The worktree might be on a different branch that will be handled later
237
- logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
238
- }
239
- }
214
+ const gitCoordinator = new GitPipelineCoordinator();
215
+ await gitCoordinator.ensureWorktree({
216
+ worktreeDir,
217
+ pipelineBranch,
218
+ repoRoot,
219
+ baseBranch: currentBranch,
220
+ });
240
221
 
241
222
  // Change current directory to worktree for all subsequent operations
242
223
  // This ensures that all spawned processes (like git or npm) inherit the correct CWD
@@ -245,7 +226,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
245
226
 
246
227
  // Create chat
247
228
  logger.info('Creating chat session...');
248
- const chatId = cursorAgentCreateChat(worktreeDir);
229
+ const agentSupervisor = new AgentSupervisor();
230
+ const chatId = agentSupervisor.createChat(worktreeDir);
249
231
 
250
232
  // Initialize state if not loaded
251
233
  if (!state) {
@@ -281,6 +263,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
281
263
  let previousTaskBranch: string | null = null;
282
264
 
283
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
+
284
270
  // Re-read tasks file to allow dynamic updates to future tasks
285
271
  try {
286
272
  const currentConfig = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
@@ -308,7 +294,15 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
308
294
  logger.warn(`⚠️ Could not reload tasks from ${tasksFile}. Using existing configuration. (${e instanceof Error ? e.message : String(e)})`);
309
295
  }
310
296
 
311
- 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
+ }
312
306
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
313
307
 
314
308
  // Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
@@ -348,7 +342,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
348
342
  onTimeout: 'fail',
349
343
  });
350
344
 
351
- await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
345
+ await gitCoordinator.mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
352
346
 
353
347
  state.status = 'running';
354
348
  state.waitingFor = [];
@@ -371,8 +365,103 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
371
365
  }
372
366
  }
373
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
+ // =========================================================================
374
463
  const result = await runTask({
375
- task,
464
+ task: currentTask,
376
465
  config,
377
466
  index: i,
378
467
  worktreeDir,
@@ -380,6 +469,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
380
469
  taskBranch,
381
470
  chatId,
382
471
  runDir,
472
+ agentSupervisor,
473
+ laneName,
383
474
  });
384
475
 
385
476
  results.push(result);
@@ -392,8 +483,74 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
392
483
  }
393
484
  saveState(statePath, state);
394
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
+
395
538
  // Handle blocked or error
396
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
+
397
554
  state.status = 'failed';
398
555
  state.dependencyRequest = result.dependencyRequest || null;
399
556
  saveState(statePath, state);
@@ -410,6 +567,24 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
410
567
  }
411
568
 
412
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
+
413
588
  state.status = 'failed';
414
589
  state.error = result.error || 'Unknown error';
415
590
  saveState(statePath, state);
@@ -418,52 +593,18 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
418
593
  }
419
594
 
420
595
  // Merge into pipeline
421
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
422
-
423
- // Ensure we are on the pipeline branch before merging the task branch
424
- logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
425
- git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
426
-
427
- // Pre-check for conflicts (should be rare since task branch was created from pipeline)
428
- const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
429
- if (conflictCheck.willConflict) {
430
- logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
431
- logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
432
- logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
433
-
434
- events.emit('merge.conflict_detected', {
596
+ try {
597
+ gitCoordinator.mergeTaskIntoPipeline({
435
598
  taskName: task.name,
436
599
  taskBranch,
437
600
  pipelineBranch,
438
- conflictingFiles: conflictCheck.conflictingFiles,
439
- preCheck: true,
601
+ worktreeDir,
440
602
  });
441
- }
442
-
443
- // Use safeMerge instead of plain merge for better error handling
444
- logger.info(`🔀 Merging task ${task.name} (${taskBranch}) into ${pipelineBranch}`);
445
- const mergeResult = git.safeMerge(taskBranch, {
446
- cwd: worktreeDir,
447
- noFf: true,
448
- message: `chore: merge task ${task.name} into pipeline`,
449
- abortOnConflict: true,
450
- });
451
-
452
- if (!mergeResult.success) {
453
- if (mergeResult.conflict) {
454
- logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
455
- state.status = 'failed';
456
- state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
457
- saveState(statePath, state);
458
- process.exit(1);
459
- }
460
- throw new Error(mergeResult.error || 'Merge failed');
461
- }
462
-
463
- // Log changed files
464
- const stats = git.getLastOperationStats(worktreeDir);
465
- if (stats) {
466
- 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);
467
608
  }
468
609
 
469
610
  git.push(pipelineBranch, { cwd: worktreeDir });
@@ -487,30 +628,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
487
628
  }
488
629
 
489
630
  // 2. Create flow branch from pipelineBranch and cleanup
490
- if (flowBranch !== pipelineBranch) {
491
- logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
492
- try {
493
- // Create/Overwrite flow branch from pipeline branch
494
- git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
495
- git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
496
-
497
- // 3. Delete temporary pipeline branch (LOCAL ONLY)
498
- // Keep remote branch for dependency lanes that may need to merge it!
499
- logger.info(`🗑️ Deleting local pipeline branch: ${pipelineBranch}`);
500
- // Must be on another branch to delete pipelineBranch
501
- git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
502
- git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
503
-
504
- // NOTE: We intentionally keep the remote pipeline branch alive
505
- // because other lanes with dependsOn may need to merge it.
506
- // The pipeline branch on remote serves as the "official" completion branch
507
- // that dependency-tracking code in task.ts uses (via state.pipelineBranch).
508
-
509
- logger.success(`✓ Flow branch '${flowBranch}' created. Remote pipeline branch preserved for dependencies.`);
510
- } catch (e) {
511
- logger.error(`❌ Failed during final consolidation: ${e}`);
512
- }
513
- }
631
+ gitCoordinator.finalizeFlowBranch({
632
+ flowBranch,
633
+ pipelineBranch,
634
+ worktreeDir,
635
+ });
514
636
 
515
637
  // Complete
516
638
  state.status = 'completed';
@@ -528,7 +650,65 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
528
650
  // Ignore
529
651
  }
530
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
+
531
712
  logger.success('All tasks completed!');
532
713
  return results;
533
714
  }
534
-