@litmers/cursorflow-orchestrator 0.1.31 โ†’ 0.1.36

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 (150) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +182 -59
  3. package/commands/cursorflow-add.md +159 -0
  4. package/commands/cursorflow-doctor.md +45 -23
  5. package/commands/cursorflow-monitor.md +23 -2
  6. package/commands/cursorflow-new.md +87 -0
  7. package/commands/cursorflow-run.md +60 -111
  8. package/dist/cli/add.d.ts +7 -0
  9. package/dist/cli/add.js +377 -0
  10. package/dist/cli/add.js.map +1 -0
  11. package/dist/cli/clean.js +1 -0
  12. package/dist/cli/clean.js.map +1 -1
  13. package/dist/cli/config.d.ts +7 -0
  14. package/dist/cli/config.js +181 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/doctor.js +47 -4
  17. package/dist/cli/doctor.js.map +1 -1
  18. package/dist/cli/index.js +34 -30
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/logs.js +17 -34
  21. package/dist/cli/logs.js.map +1 -1
  22. package/dist/cli/monitor.js +62 -65
  23. package/dist/cli/monitor.js.map +1 -1
  24. package/dist/cli/new.d.ts +7 -0
  25. package/dist/cli/new.js +232 -0
  26. package/dist/cli/new.js.map +1 -0
  27. package/dist/cli/prepare.js +95 -193
  28. package/dist/cli/prepare.js.map +1 -1
  29. package/dist/cli/resume.js +57 -68
  30. package/dist/cli/resume.js.map +1 -1
  31. package/dist/cli/run.js +60 -30
  32. package/dist/cli/run.js.map +1 -1
  33. package/dist/cli/stop.js +6 -0
  34. package/dist/cli/stop.js.map +1 -1
  35. package/dist/cli/tasks.d.ts +5 -3
  36. package/dist/cli/tasks.js +181 -29
  37. package/dist/cli/tasks.js.map +1 -1
  38. package/dist/core/failure-policy.d.ts +9 -0
  39. package/dist/core/failure-policy.js +9 -0
  40. package/dist/core/failure-policy.js.map +1 -1
  41. package/dist/core/orchestrator.d.ts +20 -6
  42. package/dist/core/orchestrator.js +215 -334
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +27 -0
  45. package/dist/core/runner/agent.js +294 -0
  46. package/dist/core/runner/agent.js.map +1 -0
  47. package/dist/core/runner/index.d.ts +5 -0
  48. package/dist/core/runner/index.js +22 -0
  49. package/dist/core/runner/index.js.map +1 -0
  50. package/dist/core/runner/pipeline.d.ts +9 -0
  51. package/dist/core/runner/pipeline.js +539 -0
  52. package/dist/core/runner/pipeline.js.map +1 -0
  53. package/dist/core/runner/prompt.d.ts +25 -0
  54. package/dist/core/runner/prompt.js +175 -0
  55. package/dist/core/runner/prompt.js.map +1 -0
  56. package/dist/core/runner/task.d.ts +26 -0
  57. package/dist/core/runner/task.js +283 -0
  58. package/dist/core/runner/task.js.map +1 -0
  59. package/dist/core/runner/utils.d.ts +37 -0
  60. package/dist/core/runner/utils.js +161 -0
  61. package/dist/core/runner/utils.js.map +1 -0
  62. package/dist/core/runner.d.ts +2 -96
  63. package/dist/core/runner.js +11 -1136
  64. package/dist/core/runner.js.map +1 -1
  65. package/dist/core/stall-detection.d.ts +326 -0
  66. package/dist/core/stall-detection.js +781 -0
  67. package/dist/core/stall-detection.js.map +1 -0
  68. package/dist/services/logging/console.js +2 -1
  69. package/dist/services/logging/console.js.map +1 -1
  70. package/dist/types/config.d.ts +6 -6
  71. package/dist/types/flow.d.ts +84 -0
  72. package/dist/types/flow.js +10 -0
  73. package/dist/types/flow.js.map +1 -0
  74. package/dist/types/index.d.ts +1 -0
  75. package/dist/types/index.js +3 -3
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/types/lane.d.ts +0 -2
  78. package/dist/types/logging.d.ts +5 -1
  79. package/dist/types/task.d.ts +7 -11
  80. package/dist/utils/config.d.ts +5 -1
  81. package/dist/utils/config.js +15 -16
  82. package/dist/utils/config.js.map +1 -1
  83. package/dist/utils/dependency.d.ts +36 -1
  84. package/dist/utils/dependency.js +256 -1
  85. package/dist/utils/dependency.js.map +1 -1
  86. package/dist/utils/doctor.js +40 -8
  87. package/dist/utils/doctor.js.map +1 -1
  88. package/dist/utils/enhanced-logger.d.ts +45 -82
  89. package/dist/utils/enhanced-logger.js +239 -844
  90. package/dist/utils/enhanced-logger.js.map +1 -1
  91. package/dist/utils/flow.d.ts +9 -0
  92. package/dist/utils/flow.js +73 -0
  93. package/dist/utils/flow.js.map +1 -0
  94. package/dist/utils/git.d.ts +29 -0
  95. package/dist/utils/git.js +115 -5
  96. package/dist/utils/git.js.map +1 -1
  97. package/dist/utils/state.js +0 -2
  98. package/dist/utils/state.js.map +1 -1
  99. package/dist/utils/task-service.d.ts +2 -2
  100. package/dist/utils/task-service.js +40 -31
  101. package/dist/utils/task-service.js.map +1 -1
  102. package/package.json +4 -3
  103. package/src/cli/add.ts +397 -0
  104. package/src/cli/clean.ts +1 -0
  105. package/src/cli/config.ts +177 -0
  106. package/src/cli/doctor.ts +48 -4
  107. package/src/cli/index.ts +36 -32
  108. package/src/cli/logs.ts +20 -33
  109. package/src/cli/monitor.ts +70 -75
  110. package/src/cli/new.ts +235 -0
  111. package/src/cli/prepare.ts +98 -205
  112. package/src/cli/resume.ts +61 -76
  113. package/src/cli/run.ts +333 -306
  114. package/src/cli/stop.ts +8 -0
  115. package/src/cli/tasks.ts +200 -21
  116. package/src/core/failure-policy.ts +9 -0
  117. package/src/core/orchestrator.ts +279 -379
  118. package/src/core/runner/agent.ts +314 -0
  119. package/src/core/runner/index.ts +6 -0
  120. package/src/core/runner/pipeline.ts +567 -0
  121. package/src/core/runner/prompt.ts +174 -0
  122. package/src/core/runner/task.ts +320 -0
  123. package/src/core/runner/utils.ts +142 -0
  124. package/src/core/runner.ts +8 -1347
  125. package/src/core/stall-detection.ts +936 -0
  126. package/src/services/logging/console.ts +2 -1
  127. package/src/types/config.ts +6 -6
  128. package/src/types/flow.ts +91 -0
  129. package/src/types/index.ts +15 -3
  130. package/src/types/lane.ts +0 -2
  131. package/src/types/logging.ts +5 -1
  132. package/src/types/task.ts +7 -11
  133. package/src/utils/config.ts +16 -17
  134. package/src/utils/dependency.ts +311 -2
  135. package/src/utils/doctor.ts +36 -8
  136. package/src/utils/enhanced-logger.ts +264 -927
  137. package/src/utils/flow.ts +42 -0
  138. package/src/utils/git.ts +145 -5
  139. package/src/utils/state.ts +0 -2
  140. package/src/utils/task-service.ts +48 -40
  141. package/commands/cursorflow-review.md +0 -56
  142. package/commands/cursorflow-runs.md +0 -59
  143. package/dist/cli/runs.d.ts +0 -5
  144. package/dist/cli/runs.js +0 -214
  145. package/dist/cli/runs.js.map +0 -1
  146. package/dist/core/reviewer.d.ts +0 -66
  147. package/dist/core/reviewer.js +0 -265
  148. package/dist/core/reviewer.js.map +0 -1
  149. package/src/cli/runs.ts +0 -212
  150. package/src/core/reviewer.ts +0 -285
@@ -0,0 +1,567 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as git from '../../utils/git';
4
+ import * as logger from '../../utils/logger';
5
+ import { ensureCursorAgent, checkCursorAuth, printAuthHelp } from '../../utils/cursor-agent';
6
+ import { saveState, loadState, validateLaneState, repairLaneState, stateNeedsRecovery } from '../../utils/state';
7
+ import { events } from '../../utils/events';
8
+ import { preflightCheck, printPreflightReport } from '../../utils/health';
9
+ import { createCheckpoint, getLatestCheckpoint } from '../../utils/checkpoint';
10
+ import { safeJoin } from '../../utils/path';
11
+ import {
12
+ RunnerConfig,
13
+ TaskExecutionResult,
14
+ LaneState
15
+ } from '../../types';
16
+ import {
17
+ cursorAgentCreateChat
18
+ } from './agent';
19
+ import {
20
+ runTask,
21
+ waitForTaskDependencies,
22
+ mergeDependencyBranches
23
+ } from './task';
24
+
25
+ /**
26
+ * Validate task configuration
27
+ */
28
+ function validateTaskConfig(config: RunnerConfig): void {
29
+ if (!config.tasks || config.tasks.length === 0) {
30
+ throw new Error('No tasks defined in configuration');
31
+ }
32
+
33
+ for (let i = 0; i < config.tasks.length; i++) {
34
+ const task = config.tasks[i]!;
35
+ if (!task.name) throw new Error(`Task at index ${i} has no name`);
36
+ if (!task.prompt) throw new Error(`Task "${task.name}" has no prompt`);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Run all tasks in sequence
42
+ */
43
+ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean; skipPreflight?: boolean } = {}): Promise<TaskExecutionResult[]> {
44
+ const startIndex = options.startIndex || 0;
45
+ const noGit = options.noGit || config.noGit || false;
46
+
47
+ // Ensure paths are absolute before potentially changing directory
48
+ runDir = path.resolve(runDir);
49
+ tasksFile = path.resolve(tasksFile);
50
+
51
+ if (noGit) {
52
+ logger.info('๐Ÿšซ Running in noGit mode - Git operations will be skipped');
53
+ }
54
+
55
+ // Validate configuration before starting
56
+ logger.info('Validating task configuration...');
57
+ try {
58
+ validateTaskConfig(config);
59
+ logger.success('โœ“ Configuration valid');
60
+ } catch (validationError: any) {
61
+ logger.error('โŒ Configuration validation failed');
62
+ logger.error(` ${validationError.message}`);
63
+ throw validationError;
64
+ }
65
+
66
+ // Run preflight checks (can be skipped for resume)
67
+ if (!options.skipPreflight && startIndex === 0) {
68
+ logger.info('Running preflight checks...');
69
+ const preflight = await preflightCheck({
70
+ requireRemote: !noGit,
71
+ requireAuth: true,
72
+ });
73
+
74
+ if (!preflight.canProceed) {
75
+ printPreflightReport(preflight);
76
+ throw new Error('Preflight check failed. Please fix the blockers above.');
77
+ }
78
+
79
+ if (preflight.warnings.length > 0) {
80
+ for (const warning of preflight.warnings) {
81
+ logger.warn(`โš ๏ธ ${warning}`);
82
+ }
83
+ }
84
+
85
+ logger.success('โœ“ Preflight checks passed');
86
+ }
87
+
88
+ // Warn if baseBranch is set in config (it will be ignored)
89
+ if (config.baseBranch) {
90
+ logger.warn(`โš ๏ธ config.baseBranch="${config.baseBranch}" will be ignored. Using current branch instead.`);
91
+ }
92
+
93
+ // Set verbose git logging
94
+ git.setVerboseGit(config.verboseGit || false);
95
+
96
+ // Ensure cursor-agent is installed
97
+ ensureCursorAgent();
98
+
99
+ // Check authentication before starting
100
+ logger.info('Checking Cursor authentication...');
101
+ const authStatus = checkCursorAuth();
102
+
103
+ if (!authStatus.authenticated) {
104
+ logger.error('โŒ Cursor authentication failed');
105
+ logger.error(` ${authStatus.message}`);
106
+
107
+ if (authStatus.details) {
108
+ logger.error(` Details: ${authStatus.details}`);
109
+ }
110
+
111
+ if (authStatus.help) {
112
+ logger.error(` ${authStatus.help}`);
113
+ }
114
+
115
+ console.log('');
116
+ printAuthHelp();
117
+
118
+ throw new Error('Cursor authentication required. Please authenticate and try again.');
119
+ }
120
+
121
+ logger.success('โœ“ Cursor authentication OK');
122
+
123
+ // In noGit mode, we don't need repoRoot - use current directory
124
+ const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
125
+
126
+ // ALWAYS use current branch as base - ignore config.baseBranch
127
+ // This ensures dependency structure is maintained in the worktree
128
+ const currentBranch = noGit ? 'main' : git.getCurrentBranch(repoRoot);
129
+ logger.info(`๐Ÿ“ Base branch: ${currentBranch} (current branch)`);
130
+
131
+ // Load existing state if resuming
132
+ const statePath = safeJoin(runDir, 'state.json');
133
+ let state: LaneState | null = null;
134
+
135
+ if (fs.existsSync(statePath)) {
136
+ // Check if state needs recovery
137
+ if (stateNeedsRecovery(statePath)) {
138
+ logger.warn('State file indicates incomplete previous run. Attempting recovery...');
139
+ const repairedState = repairLaneState(statePath);
140
+ if (repairedState) {
141
+ state = repairedState;
142
+ logger.success('โœ“ State recovered');
143
+ } else {
144
+ logger.warn('Could not recover state. Starting fresh.');
145
+ }
146
+ } else {
147
+ state = loadState<LaneState>(statePath);
148
+
149
+ // Validate loaded state
150
+ if (state) {
151
+ const validation = validateLaneState(statePath, {
152
+ checkWorktree: !noGit,
153
+ checkBranch: !noGit,
154
+ autoRepair: true,
155
+ });
156
+
157
+ if (!validation.valid) {
158
+ logger.warn(`State validation issues: ${validation.issues.join(', ')}`);
159
+ if (validation.repaired) {
160
+ logger.info('State was auto-repaired');
161
+ state = validation.repairedState || state;
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ const randomSuffix = Math.random().toString(36).substring(2, 7);
169
+ const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
170
+
171
+ // In noGit mode, use a simple local directory instead of worktree
172
+ // Flatten the path by replacing slashes with hyphens to avoid race conditions in parent directory creation
173
+ const worktreeDir = state?.worktreeDir || config.worktreeDir || (noGit
174
+ ? safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
175
+ : safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-')));
176
+
177
+ if (startIndex === 0) {
178
+ logger.section('๐Ÿš€ Starting Pipeline');
179
+ } else {
180
+ logger.section(`๐Ÿ” Resuming Pipeline from task ${startIndex + 1}`);
181
+ }
182
+
183
+ logger.info(`Pipeline Branch: ${pipelineBranch}`);
184
+ logger.info(`Worktree: ${worktreeDir}`);
185
+ logger.info(`Tasks: ${config.tasks.length}`);
186
+
187
+ // Create worktree only if starting fresh and worktree doesn't exist
188
+ if (!fs.existsSync(worktreeDir)) {
189
+ if (noGit) {
190
+ // In noGit mode, just create the directory
191
+ logger.info(`Creating work directory: ${worktreeDir}`);
192
+ fs.mkdirSync(worktreeDir, { recursive: true });
193
+ } else {
194
+ // Use a simple retry mechanism for Git worktree creation to handle potential race conditions
195
+ let retries = 3;
196
+ let lastError: Error | null = null;
197
+
198
+ while (retries > 0) {
199
+ try {
200
+ // Ensure parent directory exists before calling git worktree
201
+ const worktreeParent = path.dirname(worktreeDir);
202
+ if (!fs.existsSync(worktreeParent)) {
203
+ fs.mkdirSync(worktreeParent, { recursive: true });
204
+ }
205
+
206
+ // Always use the current branch (already captured at start) as the base branch
207
+ git.createWorktree(worktreeDir, pipelineBranch, {
208
+ baseBranch: currentBranch,
209
+ cwd: repoRoot,
210
+ });
211
+ break; // Success
212
+ } catch (e: any) {
213
+ lastError = e;
214
+ retries--;
215
+ if (retries > 0) {
216
+ const delay = Math.floor(Math.random() * 1000) + 500;
217
+ logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
218
+ await new Promise(resolve => setTimeout(resolve, delay));
219
+ }
220
+ }
221
+ }
222
+
223
+ if (retries === 0 && lastError) {
224
+ throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
225
+ }
226
+ }
227
+ } else if (!noGit) {
228
+ // If it exists but we are in Git mode, ensure it's actually a worktree and on the right branch
229
+ logger.info(`Reusing existing worktree: ${worktreeDir}`);
230
+ try {
231
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
232
+ } catch (e) {
233
+ // If checkout fails, maybe the worktree is in a weird state.
234
+ // For now, just log it. In a more robust impl, we might want to repair it.
235
+ logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
236
+ }
237
+ }
238
+
239
+ // Change current directory to worktree for all subsequent operations
240
+ // This ensures that all spawned processes (like git or npm) inherit the correct CWD
241
+ logger.info(`Changing directory to worktree: ${worktreeDir}`);
242
+ process.chdir(worktreeDir);
243
+
244
+ // Create chat
245
+ logger.info('Creating chat session...');
246
+ const chatId = cursorAgentCreateChat(worktreeDir);
247
+
248
+ // Initialize state if not loaded
249
+ if (!state) {
250
+ state = {
251
+ status: 'running',
252
+ pipelineBranch,
253
+ worktreeDir,
254
+ totalTasks: config.tasks.length,
255
+ currentTaskIndex: 0,
256
+ label: pipelineBranch,
257
+ startTime: Date.now(),
258
+ endTime: null,
259
+ error: null,
260
+ dependencyRequest: null,
261
+ tasksFile, // Store tasks file for resume
262
+ completedTasks: [],
263
+ };
264
+ } else {
265
+ state.status = 'running';
266
+ state.error = null;
267
+ state.dependencyRequest = null;
268
+ state.pipelineBranch = pipelineBranch;
269
+ state.worktreeDir = worktreeDir;
270
+ state.label = state.label || pipelineBranch;
271
+ state.completedTasks = state.completedTasks || [];
272
+ }
273
+
274
+ saveState(statePath, state);
275
+
276
+ // Run tasks
277
+ const results: TaskExecutionResult[] = [];
278
+ const laneName = state.label || path.basename(runDir);
279
+ let previousTaskBranch: string | null = null;
280
+
281
+ for (let i = startIndex; i < config.tasks.length; i++) {
282
+ // Re-read tasks file to allow dynamic updates to future tasks
283
+ try {
284
+ const currentConfig = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
285
+ if (currentConfig.tasks && currentConfig.tasks.length > i) {
286
+ // Update the current and future tasks from the file
287
+ config.tasks[i] = currentConfig.tasks[i]!;
288
+ // Also update future tasks in case the user added/removed tasks
289
+ for (let j = i + 1; j < currentConfig.tasks.length; j++) {
290
+ config.tasks[j] = currentConfig.tasks[j]!;
291
+ }
292
+ // Sync the length if tasks were added
293
+ if (currentConfig.tasks.length > config.tasks.length) {
294
+ for (let j = config.tasks.length; j < currentConfig.tasks.length; j++) {
295
+ config.tasks.push(currentConfig.tasks[j]!);
296
+ }
297
+ }
298
+ // Update total tasks count in state if it changed
299
+ if (state && state.totalTasks !== currentConfig.tasks.length) {
300
+ state.totalTasks = currentConfig.tasks.length;
301
+ saveState(statePath, state);
302
+ logger.info(`๐Ÿ“‹ Task list updated. New total tasks: ${state.totalTasks}`);
303
+ }
304
+ }
305
+ } catch (e) {
306
+ logger.warn(`โš ๏ธ Could not reload tasks from ${tasksFile}. Using existing configuration. (${e instanceof Error ? e.message : String(e)})`);
307
+ }
308
+
309
+ const task = config.tasks[i]!;
310
+ const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
311
+
312
+ // Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
313
+ if (!noGit && previousTaskBranch) {
314
+ logger.info(`๐Ÿงน Deleting previous task branch: ${previousTaskBranch}`);
315
+ try {
316
+ // Only delete if it's not the current branch
317
+ const current = git.getCurrentBranch(worktreeDir);
318
+ if (current !== previousTaskBranch) {
319
+ git.deleteBranch(previousTaskBranch, { cwd: worktreeDir, force: true });
320
+ }
321
+ } catch (e) {
322
+ logger.warn(`Failed to delete previous branch ${previousTaskBranch}: ${e}`);
323
+ }
324
+ }
325
+
326
+ // Create checkpoint before each task
327
+ try {
328
+ await createCheckpoint(laneName, runDir, noGit ? null : worktreeDir, {
329
+ description: `Before task ${i + 1}: ${task.name}`,
330
+ maxCheckpoints: 5,
331
+ });
332
+ } catch (e: any) {
333
+ logger.warn(`Failed to create checkpoint: ${e.message}`);
334
+ }
335
+
336
+ // Handle task-level dependencies
337
+ if (task.dependsOn && task.dependsOn.length > 0) {
338
+ state.status = 'waiting';
339
+ state.waitingFor = task.dependsOn;
340
+ saveState(statePath, state);
341
+
342
+ try {
343
+ // Use enhanced dependency wait with timeout
344
+ await waitForTaskDependencies(task.dependsOn, runDir, {
345
+ timeoutMs: config.timeout || 30 * 60 * 1000,
346
+ onTimeout: 'fail',
347
+ });
348
+
349
+ if (!noGit) {
350
+ await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
351
+ }
352
+
353
+ state.status = 'running';
354
+ state.waitingFor = [];
355
+ saveState(statePath, state);
356
+ } catch (e: any) {
357
+ state.status = 'failed';
358
+ state.waitingFor = [];
359
+ state.error = e.message;
360
+ saveState(statePath, state);
361
+ logger.error(`Task dependency wait/merge failed: ${e.message}`);
362
+
363
+ // Try to restore from checkpoint
364
+ const latestCheckpoint = getLatestCheckpoint(runDir);
365
+ if (latestCheckpoint) {
366
+ logger.info(`๐Ÿ’พ Checkpoint available: ${latestCheckpoint.id}`);
367
+ logger.info(` Resume with: cursorflow resume --checkpoint ${latestCheckpoint.id}`);
368
+ }
369
+
370
+ process.exit(1);
371
+ }
372
+ }
373
+
374
+ const result = await runTask({
375
+ task,
376
+ config,
377
+ index: i,
378
+ worktreeDir,
379
+ pipelineBranch,
380
+ taskBranch,
381
+ chatId,
382
+ runDir,
383
+ noGit,
384
+ });
385
+
386
+ results.push(result);
387
+
388
+ // Update state
389
+ state.currentTaskIndex = i + 1;
390
+ state.completedTasks = state.completedTasks || [];
391
+ if (!state.completedTasks.includes(task.name)) {
392
+ state.completedTasks.push(task.name);
393
+ }
394
+ saveState(statePath, state);
395
+
396
+ // Handle blocked or error
397
+ if (result.status === 'BLOCKED_DEPENDENCY') {
398
+ state.status = 'failed';
399
+ state.dependencyRequest = result.dependencyRequest || null;
400
+ saveState(statePath, state);
401
+
402
+ if (result.dependencyRequest) {
403
+ events.emit('lane.dependency_requested', {
404
+ laneName: state.label,
405
+ dependencyRequest: result.dependencyRequest,
406
+ });
407
+ }
408
+
409
+ logger.warn('Task blocked on dependency change');
410
+ process.exit(2);
411
+ }
412
+
413
+ if (result.status !== 'FINISHED') {
414
+ state.status = 'failed';
415
+ state.error = result.error || 'Unknown error';
416
+ saveState(statePath, state);
417
+ logger.error(`Task failed: ${result.error}`);
418
+ process.exit(1);
419
+ }
420
+
421
+ // Merge into pipeline (skip in noGit mode)
422
+ if (!noGit) {
423
+ logger.info(`Merging ${taskBranch} โ†’ ${pipelineBranch}`);
424
+
425
+ // Ensure we are on the pipeline branch before merging the task branch
426
+ logger.info(`๐Ÿ”„ Switching to pipeline branch ${pipelineBranch} to integrate changes`);
427
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
428
+
429
+ // Pre-check for conflicts (should be rare since task branch was created from pipeline)
430
+ const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
431
+ if (conflictCheck.willConflict) {
432
+ logger.warn(`โš ๏ธ Unexpected conflict detected when merging ${taskBranch}`);
433
+ logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
434
+ logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
435
+
436
+ events.emit('merge.conflict_detected', {
437
+ taskName: task.name,
438
+ taskBranch,
439
+ pipelineBranch,
440
+ conflictingFiles: conflictCheck.conflictingFiles,
441
+ preCheck: true,
442
+ });
443
+ }
444
+
445
+ // Use safeMerge instead of plain merge for better error handling
446
+ logger.info(`๐Ÿ”€ Merging task ${task.name} (${taskBranch}) into ${pipelineBranch}`);
447
+ const mergeResult = git.safeMerge(taskBranch, {
448
+ cwd: worktreeDir,
449
+ noFf: true,
450
+ message: `chore: merge task ${task.name} into pipeline`,
451
+ abortOnConflict: true,
452
+ });
453
+
454
+ if (!mergeResult.success) {
455
+ if (mergeResult.conflict) {
456
+ logger.error(`โŒ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
457
+ state.status = 'failed';
458
+ state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
459
+ saveState(statePath, state);
460
+ process.exit(1);
461
+ }
462
+ throw new Error(mergeResult.error || 'Merge failed');
463
+ }
464
+
465
+ // Log changed files
466
+ const stats = git.getLastOperationStats(worktreeDir);
467
+ if (stats) {
468
+ logger.info('Changed files:\n' + stats);
469
+ }
470
+
471
+ git.push(pipelineBranch, { cwd: worktreeDir });
472
+ } else {
473
+ logger.info(`โœ“ Task ${task.name} completed (noGit mode - no branch operations)`);
474
+ }
475
+
476
+ // Set previousTaskBranch for cleanup in the next iteration
477
+ previousTaskBranch = noGit ? null : taskBranch;
478
+ }
479
+
480
+ // Final Consolidation: Create flow branch and cleanup
481
+ if (!noGit) {
482
+ const flowBranch = laneName;
483
+ logger.section(`๐Ÿ Final Consolidation: ${flowBranch}`);
484
+
485
+ // 1. Delete the very last task branch
486
+ if (previousTaskBranch) {
487
+ logger.info(`๐Ÿงน Deleting last task branch: ${previousTaskBranch}`);
488
+ try {
489
+ git.deleteBranch(previousTaskBranch, { cwd: worktreeDir, force: true });
490
+ } catch (e) {
491
+ logger.warn(` Failed to delete last task branch: ${e}`);
492
+ }
493
+ }
494
+
495
+ // 2. Create flow branch from pipelineBranch and cleanup
496
+ if (flowBranch !== pipelineBranch) {
497
+ logger.info(`๐ŸŒฟ Creating final flow branch: ${flowBranch}`);
498
+ try {
499
+ // Create/Overwrite flow branch from pipeline branch
500
+ git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
501
+ git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
502
+
503
+ // 3. Delete temporary pipeline branch
504
+ logger.info(`๐Ÿ—‘๏ธ Deleting temporary pipeline branch: ${pipelineBranch}`);
505
+ // Must be on another branch to delete pipelineBranch
506
+ git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
507
+ git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
508
+
509
+ try {
510
+ git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true, remote: true });
511
+ logger.info(` Deleted remote branch: origin/${pipelineBranch}`);
512
+ } catch {
513
+ // May not exist on remote or delete failed
514
+ }
515
+
516
+ logger.success(`โœ“ Flow branch '${flowBranch}' is now the only remaining branch.`);
517
+ } catch (e) {
518
+ logger.error(`โŒ Failed during final consolidation: ${e}`);
519
+ }
520
+ }
521
+ }
522
+
523
+ // Complete
524
+ state.status = 'completed';
525
+ state.endTime = Date.now();
526
+ saveState(statePath, state);
527
+
528
+ // Log final file summary
529
+ if (noGit) {
530
+ const getFileSummary = (dir: string): { files: number; dirs: number } => {
531
+ let stats = { files: 0, dirs: 0 };
532
+ if (!fs.existsSync(dir)) return stats;
533
+
534
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
535
+ for (const entry of entries) {
536
+ if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
537
+
538
+ if (entry.isDirectory()) {
539
+ stats.dirs++;
540
+ const sub = getFileSummary(safeJoin(dir, entry.name));
541
+ stats.files += sub.files;
542
+ stats.dirs += sub.dirs;
543
+ } else {
544
+ stats.files++;
545
+ }
546
+ }
547
+ return stats;
548
+ };
549
+
550
+ const summary = getFileSummary(worktreeDir);
551
+ logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
552
+ } else {
553
+ try {
554
+ // Always use current branch for comparison (already captured at start)
555
+ const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
556
+ if (stats) {
557
+ logger.info('Final Workspace Summary (Git):\n' + stats);
558
+ }
559
+ } catch (e) {
560
+ // Ignore
561
+ }
562
+ }
563
+
564
+ logger.success('All tasks completed!');
565
+ return results;
566
+ }
567
+