@litmers/cursorflow-orchestrator 0.1.37 → 0.1.40

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 (102) hide show
  1. package/README.md +13 -13
  2. package/commands/cursorflow-init.md +113 -32
  3. package/commands/cursorflow-prepare.md +146 -339
  4. package/commands/cursorflow-run.md +148 -131
  5. package/dist/cli/add.js +8 -4
  6. package/dist/cli/add.js.map +1 -1
  7. package/dist/cli/index.js +2 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/new.js +3 -5
  10. package/dist/cli/new.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 +24 -15
  14. package/dist/cli/resume.js.map +1 -1
  15. package/dist/cli/run.js +1 -6
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/cli/setup-commands.d.ts +1 -0
  18. package/dist/cli/setup-commands.js +1 -0
  19. package/dist/cli/setup-commands.js.map +1 -1
  20. package/dist/core/orchestrator.js +13 -5
  21. package/dist/core/orchestrator.js.map +1 -1
  22. package/dist/core/runner/agent.d.ts +5 -1
  23. package/dist/core/runner/agent.js +31 -1
  24. package/dist/core/runner/agent.js.map +1 -1
  25. package/dist/core/runner/pipeline.d.ts +0 -1
  26. package/dist/core/runner/pipeline.js +136 -173
  27. package/dist/core/runner/pipeline.js.map +1 -1
  28. package/dist/core/runner/prompt.d.ts +0 -1
  29. package/dist/core/runner/prompt.js +11 -16
  30. package/dist/core/runner/prompt.js.map +1 -1
  31. package/dist/core/runner/task.d.ts +1 -2
  32. package/dist/core/runner/task.js +31 -40
  33. package/dist/core/runner/task.js.map +1 -1
  34. package/dist/core/runner.js +15 -2
  35. package/dist/core/runner.js.map +1 -1
  36. package/dist/core/stall-detection.d.ts +32 -4
  37. package/dist/core/stall-detection.js +151 -149
  38. package/dist/core/stall-detection.js.map +1 -1
  39. package/dist/services/logging/console.d.ts +7 -1
  40. package/dist/services/logging/console.js +13 -3
  41. package/dist/services/logging/console.js.map +1 -1
  42. package/dist/services/logging/formatter.d.ts +1 -0
  43. package/dist/services/logging/formatter.js +6 -3
  44. package/dist/services/logging/formatter.js.map +1 -1
  45. package/dist/types/config.d.ts +3 -1
  46. package/dist/types/logging.d.ts +1 -1
  47. package/dist/types/task.d.ts +3 -8
  48. package/dist/utils/config.js +5 -0
  49. package/dist/utils/config.js.map +1 -1
  50. package/dist/utils/doctor.js +4 -4
  51. package/dist/utils/doctor.js.map +1 -1
  52. package/dist/utils/enhanced-logger.d.ts +1 -1
  53. package/dist/utils/enhanced-logger.js +3 -3
  54. package/dist/utils/enhanced-logger.js.map +1 -1
  55. package/dist/utils/git.d.ts +12 -1
  56. package/dist/utils/git.js +56 -1
  57. package/dist/utils/git.js.map +1 -1
  58. package/dist/utils/health.js +13 -13
  59. package/dist/utils/health.js.map +1 -1
  60. package/dist/utils/log-formatter.d.ts +1 -1
  61. package/dist/utils/log-formatter.js +45 -8
  62. package/dist/utils/log-formatter.js.map +1 -1
  63. package/dist/utils/logger.js +2 -2
  64. package/dist/utils/logger.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/cli/add.ts +9 -4
  67. package/src/cli/index.ts +3 -0
  68. package/src/cli/new.ts +3 -5
  69. package/src/cli/prepare.ts +0 -1
  70. package/src/cli/resume.ts +28 -19
  71. package/src/cli/run.ts +1 -6
  72. package/src/cli/setup-commands.ts +1 -1
  73. package/src/core/orchestrator.ts +14 -5
  74. package/src/core/runner/agent.ts +36 -4
  75. package/src/core/runner/pipeline.ts +149 -182
  76. package/src/core/runner/prompt.ts +11 -18
  77. package/src/core/runner/task.ts +32 -41
  78. package/src/core/runner.ts +17 -2
  79. package/src/core/stall-detection.ts +263 -147
  80. package/src/services/logging/console.ts +13 -3
  81. package/src/services/logging/formatter.ts +6 -3
  82. package/src/types/config.ts +3 -1
  83. package/src/types/logging.ts +4 -2
  84. package/src/types/task.ts +3 -8
  85. package/src/utils/config.ts +6 -0
  86. package/src/utils/doctor.ts +5 -5
  87. package/src/utils/enhanced-logger.ts +3 -3
  88. package/src/utils/flow.ts +1 -0
  89. package/src/utils/git.ts +61 -1
  90. package/src/utils/health.ts +15 -15
  91. package/src/utils/log-formatter.ts +51 -8
  92. package/src/utils/logger.ts +2 -2
  93. package/commands/cursorflow-add.md +0 -159
  94. package/commands/cursorflow-clean.md +0 -84
  95. package/commands/cursorflow-doctor.md +0 -102
  96. package/commands/cursorflow-models.md +0 -51
  97. package/commands/cursorflow-monitor.md +0 -90
  98. package/commands/cursorflow-new.md +0 -87
  99. package/commands/cursorflow-resume.md +0 -205
  100. package/commands/cursorflow-signal.md +0 -52
  101. package/commands/cursorflow-stop.md +0 -55
  102. package/commands/cursorflow-triggers.md +0 -250
@@ -40,18 +40,13 @@ function validateTaskConfig(config: RunnerConfig): void {
40
40
  /**
41
41
  * Run all tasks in sequence
42
42
  */
43
- export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean; skipPreflight?: boolean } = {}): Promise<TaskExecutionResult[]> {
43
+ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; skipPreflight?: boolean } = {}): Promise<TaskExecutionResult[]> {
44
44
  const startIndex = options.startIndex || 0;
45
- const noGit = options.noGit || config.noGit || false;
46
45
 
47
46
  // Ensure paths are absolute before potentially changing directory
48
47
  runDir = path.resolve(runDir);
49
48
  tasksFile = path.resolve(tasksFile);
50
49
 
51
- if (noGit) {
52
- logger.info('🚫 Running in noGit mode - Git operations will be skipped');
53
- }
54
-
55
50
  // Validate configuration before starting
56
51
  logger.info('Validating task configuration...');
57
52
  try {
@@ -67,7 +62,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
67
62
  if (!options.skipPreflight && startIndex === 0) {
68
63
  logger.info('Running preflight checks...');
69
64
  const preflight = await preflightCheck({
70
- requireRemote: !noGit,
65
+ requireRemote: true,
71
66
  requireAuth: true,
72
67
  });
73
68
 
@@ -120,12 +115,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
120
115
 
121
116
  logger.success('✓ Cursor authentication OK');
122
117
 
123
- // In noGit mode, we don't need repoRoot - use current directory
124
- const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
118
+ const repoRoot = git.getMainRepoRoot();
125
119
 
126
120
  // ALWAYS use current branch as base - ignore config.baseBranch
127
121
  // This ensures dependency structure is maintained in the worktree
128
- const currentBranch = noGit ? 'main' : git.getCurrentBranch(repoRoot);
122
+ const currentBranch = git.getCurrentBranch(repoRoot);
129
123
  logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
130
124
 
131
125
  // Load existing state if resuming
@@ -149,8 +143,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
149
143
  // Validate loaded state
150
144
  if (state) {
151
145
  const validation = validateLaneState(statePath, {
152
- checkWorktree: !noGit,
153
- checkBranch: !noGit,
146
+ checkWorktree: true,
147
+ checkBranch: true,
154
148
  autoRepair: true,
155
149
  });
156
150
 
@@ -168,11 +162,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
168
162
  const randomSuffix = Math.random().toString(36).substring(2, 7);
169
163
  const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
170
164
 
171
- // In noGit mode, use a simple local directory instead of worktree
172
165
  // 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, '-')));
166
+ const worktreeDir = state?.worktreeDir || config.worktreeDir || safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-'));
176
167
 
177
168
  if (startIndex === 0) {
178
169
  logger.section('🚀 Starting Pipeline');
@@ -184,54 +175,65 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
184
175
  logger.info(`Worktree: ${worktreeDir}`);
185
176
  logger.info(`Tasks: ${config.tasks.length}`);
186
177
 
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
- }
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
+ }
205
208
 
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
- }
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));
220
222
  }
221
223
  }
222
-
223
- if (retries === 0 && lastError) {
224
- throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
225
- }
226
224
  }
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
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
229
231
  logger.info(`Reusing existing worktree: ${worktreeDir}`);
230
232
  try {
231
233
  git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
232
234
  } 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
+ // 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
235
237
  logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
236
238
  }
237
239
  }
@@ -310,7 +312,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
310
312
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
311
313
 
312
314
  // Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
313
- if (!noGit && previousTaskBranch) {
315
+ if (previousTaskBranch) {
314
316
  logger.info(`🧹 Deleting previous task branch: ${previousTaskBranch}`);
315
317
  try {
316
318
  // Only delete if it's not the current branch
@@ -325,7 +327,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
325
327
 
326
328
  // Create checkpoint before each task
327
329
  try {
328
- await createCheckpoint(laneName, runDir, noGit ? null : worktreeDir, {
330
+ await createCheckpoint(laneName, runDir, worktreeDir, {
329
331
  description: `Before task ${i + 1}: ${task.name}`,
330
332
  maxCheckpoints: 5,
331
333
  });
@@ -346,9 +348,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
346
348
  onTimeout: 'fail',
347
349
  });
348
350
 
349
- if (!noGit) {
350
- await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
351
- }
351
+ await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
352
352
 
353
353
  state.status = 'running';
354
354
  state.waitingFor = [];
@@ -380,7 +380,6 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
380
380
  taskBranch,
381
381
  chatId,
382
382
  runDir,
383
- noGit,
384
383
  });
385
384
 
386
385
  results.push(result);
@@ -418,105 +417,98 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
418
417
  process.exit(1);
419
418
  }
420
419
 
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
- }
420
+ // 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}`);
444
433
 
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,
434
+ events.emit('merge.conflict_detected', {
435
+ taskName: task.name,
436
+ taskBranch,
437
+ pipelineBranch,
438
+ conflictingFiles: conflictCheck.conflictingFiles,
439
+ preCheck: true,
452
440
  });
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);
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);
469
459
  }
470
-
471
- git.push(pipelineBranch, { cwd: worktreeDir });
472
- } else {
473
- logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
460
+ throw new Error(mergeResult.error || 'Merge failed');
474
461
  }
475
462
 
463
+ // Log changed files
464
+ const stats = git.getLastOperationStats(worktreeDir);
465
+ if (stats) {
466
+ logger.info('Changed files:\n' + stats);
467
+ }
468
+
469
+ git.push(pipelineBranch, { cwd: worktreeDir });
470
+
476
471
  // Set previousTaskBranch for cleanup in the next iteration
477
- previousTaskBranch = noGit ? null : taskBranch;
472
+ previousTaskBranch = taskBranch;
478
473
  }
479
474
 
480
475
  // 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
- }
476
+ const flowBranch = laneName;
477
+ logger.section(`🏁 Final Consolidation: ${flowBranch}`);
478
+
479
+ // 1. Delete the very last task branch
480
+ if (previousTaskBranch) {
481
+ logger.info(`🧹 Deleting last task branch: ${previousTaskBranch}`);
482
+ try {
483
+ git.deleteBranch(previousTaskBranch, { cwd: worktreeDir, force: true });
484
+ } catch (e) {
485
+ logger.warn(` Failed to delete last task branch: ${e}`);
493
486
  }
487
+ }
494
488
 
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
- }
489
+ // 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}`);
520
512
  }
521
513
  }
522
514
 
@@ -526,39 +518,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
526
518
  saveState(statePath, state);
527
519
 
528
520
  // 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
521
+ try {
522
+ // Always use current branch for comparison (already captured at start)
523
+ const finalStats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
524
+ if (finalStats) {
525
+ logger.info('Final Workspace Summary:\n' + finalStats);
561
526
  }
527
+ } catch (e) {
528
+ // Ignore
562
529
  }
563
530
 
564
531
  logger.success('All tasks completed!');
@@ -32,7 +32,6 @@ export function wrapPrompt(
32
32
  prompt: string,
33
33
  config: RunnerConfig,
34
34
  options: {
35
- noGit?: boolean;
36
35
  isWorktree?: boolean;
37
36
  dependencyResults?: DependencyResult[];
38
37
  worktreePath?: string;
@@ -40,18 +39,18 @@ export function wrapPrompt(
40
39
  pipelineBranch?: string;
41
40
  } = {}
42
41
  ): string {
43
- const { noGit = false, isWorktree = true, dependencyResults = [], worktreePath, taskBranch, pipelineBranch } = options;
42
+ const { isWorktree = true, dependencyResults = [], worktreePath, taskBranch, pipelineBranch } = options;
44
43
 
45
44
  // 1. PREFIX: Environment & Worktree context
46
45
  let wrapped = `### 🛠 Environment & Context\n`;
47
46
  wrapped += `- **Workspace**: 당신은 독립된 **Git 워크트리** (프로젝트 루트)에서 작업 중입니다.\n`;
48
47
  wrapped += `- **CWD**: 현재 터미널과 작업 경로는 이미 워크트리 루트(\`${worktreePath || 'current'}\`)로 설정되어 있습니다.\n`;
49
48
 
50
- if (taskBranch && !noGit) {
49
+ if (taskBranch) {
51
50
  wrapped += `- **Current Branch**: \`${taskBranch}\` (현재 작업 중인 브랜치)\n`;
52
51
  wrapped += `- **Branch Check**: 만약 브랜치가 다르다면 \`git checkout ${taskBranch}\`를 실행하세요.\n`;
53
52
  }
54
- if (pipelineBranch && !noGit) {
53
+ if (pipelineBranch) {
55
54
  wrapped += `- **Base Branch**: \`${pipelineBranch}\` (이 작업의 기준이 되는 상위 브랜치)\n`;
56
55
  }
57
56
 
@@ -97,10 +96,6 @@ export function wrapPrompt(
97
96
  wrapped += `\n### 📦 Dependency Policy\n`;
98
97
  wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
99
98
  wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
100
-
101
- if (noGit) {
102
- wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
103
- }
104
99
 
105
100
  wrapped += `\n**📦 Dependency Change Rules:**\n`;
106
101
  wrapped += `1. 코드를 수정하기 전, 의존성 변경이 필요한지 **먼저** 판단하세요.\n`;
@@ -124,16 +119,14 @@ export function wrapPrompt(
124
119
  wrapped += `\n### 📝 Task Completion Requirements\n`;
125
120
  wrapped += `**반드시 다음 순서로 작업을 마무리하세요 (매우 중요):**\n\n`;
126
121
 
127
- if (!noGit) {
128
- wrapped += `1. **변경 사항 확인**: \`git status\`와 \`git diff\`로 수정된 내용을 최종 확인하세요.\n`;
129
- wrapped += `2. **Git Commit & Push** (필수!):\n`;
130
- wrapped += ` \`\`\`bash\n`;
131
- wrapped += ` git add -A\n`;
132
- wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
133
- wrapped += ` git push origin HEAD\n`;
134
- wrapped += ` \`\`\`\n`;
135
- wrapped += ` ⚠️ **주의**: 커밋과 푸시를 생략하면 오케스트레이터가 변경 사항을 인식하지 못하며 작업이 손실됩니다.\n\n`;
136
- }
122
+ wrapped += `1. **변경 사항 확인**: \`git status\`와 \`git diff\`로 수정된 내용을 최종 확인하세요.\n`;
123
+ wrapped += `2. **Git Commit & Push** (필수!):\n`;
124
+ wrapped += ` \`\`\`bash\n`;
125
+ wrapped += ` git add -A\n`;
126
+ wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
127
+ wrapped += ` git push origin HEAD\n`;
128
+ wrapped += ` \`\`\`\n`;
129
+ wrapped += ` ⚠️ **주의**: 커밋과 푸시를 생략하면 오케스트레이터가 변경 사항을 인식하지 못하며 작업이 손실됩니다.\n\n`;
137
130
 
138
131
  wrapped += `3. **최종 요약**: 작업 완료 후 아래 형식을 포함하여 요약해 주세요:\n`;
139
132
  wrapped += ` - **수정된 파일**: [파일명1, 파일명2, ...]\n`;
@@ -79,11 +79,15 @@ export async function mergeDependencyBranches(deps: string[], runDir: string, wo
79
79
 
80
80
  logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
81
81
 
82
- // Ensure we have the latest
82
+ // Ensure we have the latest from remote
83
83
  git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
84
84
 
85
+ // Use the remote ref for merging (origin/<branch>) since dependency branches
86
+ // are pushed to remote by other lanes and may not exist as local branches
87
+ const remoteBranchRef = `origin/${state.pipelineBranch}`;
88
+
85
89
  // Pre-check for conflicts before attempting merge
86
- const conflictCheck = git.checkMergeConflict(state.pipelineBranch, { cwd: worktreeDir });
90
+ const conflictCheck = git.checkMergeConflict(remoteBranchRef, { cwd: worktreeDir });
87
91
 
88
92
  if (conflictCheck.willConflict) {
89
93
  logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`);
@@ -100,8 +104,8 @@ export async function mergeDependencyBranches(deps: string[], runDir: string, wo
100
104
  throw new Error(`Pre-merge conflict check failed: ${conflictCheck.conflictingFiles.join(', ')}. Consider rebasing or resolving conflicts manually.`);
101
105
  }
102
106
 
103
- // Use safe merge with conflict detection
104
- const mergeResult = git.safeMerge(state.pipelineBranch, {
107
+ // Use safe merge with conflict detection - merge from remote ref
108
+ const mergeResult = git.safeMerge(remoteBranchRef, {
105
109
  cwd: worktreeDir,
106
110
  noFf: true,
107
111
  message: `chore: merge task dependency from ${laneName}`,
@@ -137,7 +141,6 @@ export async function runTask({
137
141
  chatId,
138
142
  runDir,
139
143
  runRoot,
140
- noGit = false,
141
144
  }: {
142
145
  task: Task;
143
146
  config: RunnerConfig;
@@ -148,7 +151,6 @@ export async function runTask({
148
151
  chatId: string;
149
152
  runDir: string;
150
153
  runRoot?: string;
151
- noGit?: boolean;
152
154
  }): Promise<TaskExecutionResult> {
153
155
  // Calculate runRoot if not provided (runDir is lanes/{laneName}/, runRoot is parent of lanes/)
154
156
  const calculatedRunRoot = runRoot || path.dirname(path.dirname(runDir));
@@ -158,11 +160,7 @@ export async function runTask({
158
160
 
159
161
  logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
160
162
  logger.info(`Model: ${model}`);
161
- if (noGit) {
162
- logger.info('🚫 noGit mode: skipping branch operations');
163
- } else {
164
- logger.info(`Branch: ${taskBranch}`);
165
- }
163
+ logger.info(`Branch: ${taskBranch}`);
166
164
 
167
165
  events.emit('task.started', {
168
166
  taskName: task.name,
@@ -170,32 +168,28 @@ export async function runTask({
170
168
  index,
171
169
  });
172
170
 
173
- // Sync pipelineBranch with remote before starting (skip in noGit mode)
174
- if (!noGit) {
175
- logger.info(`🔄 Syncing ${pipelineBranch} with remote...`);
176
-
177
- // Fetch latest from remote
178
- try {
179
- git.runGit(['fetch', 'origin', pipelineBranch], { cwd: worktreeDir, silent: true });
180
- } catch {
181
- // Branch might not exist on remote yet - that's OK
182
- logger.info(` Branch ${pipelineBranch} not yet on remote, skipping sync`);
183
- }
184
-
185
- // Try to fast-forward if behind
186
- const syncResult = git.syncBranchWithRemote(pipelineBranch, { cwd: worktreeDir, createIfMissing: true });
187
- if (syncResult.updated) {
188
- logger.info(` ✓ Updated ${pipelineBranch} with ${syncResult.behind || 0} new commits from remote`);
189
- } else if (syncResult.error) {
190
- logger.warn(` ⚠️ Could not sync: ${syncResult.error}`);
191
- }
171
+ // Sync pipelineBranch with remote before starting
172
+ logger.info(`🔄 Syncing ${pipelineBranch} with remote...`);
173
+
174
+ // Fetch latest from remote
175
+ try {
176
+ git.runGit(['fetch', 'origin', pipelineBranch], { cwd: worktreeDir, silent: true });
177
+ } catch {
178
+ // Branch might not exist on remote yet - that's OK
179
+ logger.info(` Branch ${pipelineBranch} not yet on remote, skipping sync`);
192
180
  }
193
-
194
- // Checkout task branch from pipeline branch (skip in noGit mode)
195
- if (!noGit) {
196
- logger.info(`🌿 Forking task branch: ${taskBranch} from ${pipelineBranch}`);
197
- git.runGit(['checkout', '-B', taskBranch, pipelineBranch], { cwd: worktreeDir });
181
+
182
+ // Try to fast-forward if behind
183
+ const syncResult = git.syncBranchWithRemote(pipelineBranch, { cwd: worktreeDir, createIfMissing: true });
184
+ if (syncResult.updated) {
185
+ logger.info(` ✓ Updated ${pipelineBranch} with ${syncResult.behind || 0} new commits from remote`);
186
+ } else if (syncResult.error) {
187
+ logger.warn(` ⚠️ Could not sync: ${syncResult.error}`);
198
188
  }
189
+
190
+ // Checkout task branch from pipeline branch
191
+ logger.info(`🌿 Forking task branch: ${taskBranch} from ${pipelineBranch}`);
192
+ git.runGit(['checkout', '-B', taskBranch, pipelineBranch], { cwd: worktreeDir });
199
193
 
200
194
  // Apply dependency permissions
201
195
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
@@ -208,8 +202,7 @@ export async function runTask({
208
202
 
209
203
  // Wrap prompt with context, dependency results, and completion instructions
210
204
  const wrappedPrompt = wrapPrompt(task.prompt, config, {
211
- noGit,
212
- isWorktree: !noGit,
205
+ isWorktree: true,
213
206
  dependencyResults,
214
207
  worktreePath: worktreeDir,
215
208
  taskBranch,
@@ -297,10 +290,8 @@ export async function runTask({
297
290
  }
298
291
  }
299
292
 
300
- // Push task branch (skip in noGit mode)
301
- if (!noGit) {
302
- git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
303
- }
293
+ // Push task branch
294
+ git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
304
295
 
305
296
  // Save task result for dependency handoff
306
297
  saveTaskResult(runDir, index, task.name, r1.resultText || '');