@litmers/cursorflow-orchestrator 0.1.37 → 0.1.39

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 (84) hide show
  1. package/commands/cursorflow-init.md +113 -32
  2. package/commands/cursorflow-prepare.md +146 -339
  3. package/commands/cursorflow-run.md +148 -131
  4. package/dist/cli/add.js +8 -4
  5. package/dist/cli/add.js.map +1 -1
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/new.js +3 -5
  9. package/dist/cli/new.js.map +1 -1
  10. package/dist/cli/resume.js +26 -15
  11. package/dist/cli/resume.js.map +1 -1
  12. package/dist/cli/run.js +1 -6
  13. package/dist/cli/run.js.map +1 -1
  14. package/dist/cli/setup-commands.d.ts +1 -0
  15. package/dist/cli/setup-commands.js +1 -0
  16. package/dist/cli/setup-commands.js.map +1 -1
  17. package/dist/core/runner/agent.d.ts +5 -1
  18. package/dist/core/runner/agent.js +31 -1
  19. package/dist/core/runner/agent.js.map +1 -1
  20. package/dist/core/runner/pipeline.d.ts +0 -1
  21. package/dist/core/runner/pipeline.js +116 -167
  22. package/dist/core/runner/pipeline.js.map +1 -1
  23. package/dist/core/runner/prompt.d.ts +0 -1
  24. package/dist/core/runner/prompt.js +11 -16
  25. package/dist/core/runner/prompt.js.map +1 -1
  26. package/dist/core/runner/task.d.ts +1 -2
  27. package/dist/core/runner/task.js +24 -36
  28. package/dist/core/runner/task.js.map +1 -1
  29. package/dist/core/runner.js +12 -2
  30. package/dist/core/runner.js.map +1 -1
  31. package/dist/core/stall-detection.d.ts +16 -4
  32. package/dist/core/stall-detection.js +97 -148
  33. package/dist/core/stall-detection.js.map +1 -1
  34. package/dist/services/logging/console.d.ts +7 -1
  35. package/dist/services/logging/console.js +15 -3
  36. package/dist/services/logging/console.js.map +1 -1
  37. package/dist/services/logging/formatter.js +2 -0
  38. package/dist/services/logging/formatter.js.map +1 -1
  39. package/dist/types/config.d.ts +1 -1
  40. package/dist/types/logging.d.ts +1 -1
  41. package/dist/types/task.d.ts +2 -7
  42. package/dist/utils/doctor.js +4 -4
  43. package/dist/utils/doctor.js.map +1 -1
  44. package/dist/utils/git.js +2 -0
  45. package/dist/utils/git.js.map +1 -1
  46. package/dist/utils/health.js +13 -13
  47. package/dist/utils/health.js.map +1 -1
  48. package/dist/utils/log-formatter.js +44 -7
  49. package/dist/utils/log-formatter.js.map +1 -1
  50. package/dist/utils/logger.js +2 -2
  51. package/dist/utils/logger.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/cli/add.ts +9 -4
  54. package/src/cli/index.ts +3 -0
  55. package/src/cli/new.ts +3 -5
  56. package/src/cli/resume.ts +30 -19
  57. package/src/cli/run.ts +1 -6
  58. package/src/cli/setup-commands.ts +1 -1
  59. package/src/core/runner/agent.ts +36 -4
  60. package/src/core/runner/pipeline.ts +127 -176
  61. package/src/core/runner/prompt.ts +11 -18
  62. package/src/core/runner/task.ts +24 -37
  63. package/src/core/runner.ts +13 -2
  64. package/src/core/stall-detection.ts +190 -146
  65. package/src/services/logging/console.ts +15 -3
  66. package/src/services/logging/formatter.ts +2 -0
  67. package/src/types/config.ts +1 -1
  68. package/src/types/logging.ts +4 -2
  69. package/src/types/task.ts +2 -7
  70. package/src/utils/doctor.ts +5 -5
  71. package/src/utils/git.ts +2 -0
  72. package/src/utils/health.ts +15 -15
  73. package/src/utils/log-formatter.ts +50 -7
  74. package/src/utils/logger.ts +2 -2
  75. package/commands/cursorflow-add.md +0 -159
  76. package/commands/cursorflow-clean.md +0 -84
  77. package/commands/cursorflow-doctor.md +0 -102
  78. package/commands/cursorflow-models.md +0 -51
  79. package/commands/cursorflow-monitor.md +0 -90
  80. package/commands/cursorflow-new.md +0 -87
  81. package/commands/cursorflow-resume.md +0 -205
  82. package/commands/cursorflow-signal.md +0 -52
  83. package/commands/cursorflow-stop.md +0 -55
  84. 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');
@@ -186,46 +177,40 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
186
177
 
187
178
  // Create worktree only if starting fresh and worktree doesn't exist
188
179
  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
- }
180
+ // Use a simple retry mechanism for Git worktree creation to handle potential race conditions
181
+ let retries = 3;
182
+ let lastError: Error | null = null;
183
+
184
+ while (retries > 0) {
185
+ try {
186
+ // Ensure parent directory exists before calling git worktree
187
+ const worktreeParent = path.dirname(worktreeDir);
188
+ if (!fs.existsSync(worktreeParent)) {
189
+ fs.mkdirSync(worktreeParent, { recursive: true });
190
+ }
205
191
 
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
- }
192
+ // Always use the current branch (already captured at start) as the base branch
193
+ await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
194
+ baseBranch: currentBranch,
195
+ cwd: repoRoot,
196
+ });
197
+ break; // Success
198
+ } catch (e: any) {
199
+ lastError = e;
200
+ retries--;
201
+ if (retries > 0) {
202
+ const delay = Math.floor(Math.random() * 1000) + 500;
203
+ logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
204
+ await new Promise(resolve => setTimeout(resolve, delay));
220
205
  }
221
206
  }
222
-
223
- if (retries === 0 && lastError) {
224
- throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
225
- }
226
207
  }
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
208
+
209
+ if (retries === 0 && lastError) {
210
+ throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
211
+ }
212
+ } else {
213
+ // If it exists, ensure it's actually a worktree and on the right branch
229
214
  logger.info(`Reusing existing worktree: ${worktreeDir}`);
230
215
  try {
231
216
  git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
@@ -310,7 +295,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
310
295
  const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
311
296
 
312
297
  // Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
313
- if (!noGit && previousTaskBranch) {
298
+ if (previousTaskBranch) {
314
299
  logger.info(`🧹 Deleting previous task branch: ${previousTaskBranch}`);
315
300
  try {
316
301
  // Only delete if it's not the current branch
@@ -325,7 +310,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
325
310
 
326
311
  // Create checkpoint before each task
327
312
  try {
328
- await createCheckpoint(laneName, runDir, noGit ? null : worktreeDir, {
313
+ await createCheckpoint(laneName, runDir, worktreeDir, {
329
314
  description: `Before task ${i + 1}: ${task.name}`,
330
315
  maxCheckpoints: 5,
331
316
  });
@@ -346,9 +331,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
346
331
  onTimeout: 'fail',
347
332
  });
348
333
 
349
- if (!noGit) {
350
- await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
351
- }
334
+ await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
352
335
 
353
336
  state.status = 'running';
354
337
  state.waitingFor = [];
@@ -380,7 +363,6 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
380
363
  taskBranch,
381
364
  chatId,
382
365
  runDir,
383
- noGit,
384
366
  });
385
367
 
386
368
  results.push(result);
@@ -418,105 +400,99 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
418
400
  process.exit(1);
419
401
  }
420
402
 
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
- }
403
+ // Merge into pipeline
404
+ logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
405
+
406
+ // Ensure we are on the pipeline branch before merging the task branch
407
+ logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
408
+ git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
409
+
410
+ // Pre-check for conflicts (should be rare since task branch was created from pipeline)
411
+ const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
412
+ if (conflictCheck.willConflict) {
413
+ logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
414
+ logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
415
+ logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
444
416
 
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,
417
+ events.emit('merge.conflict_detected', {
418
+ taskName: task.name,
419
+ taskBranch,
420
+ pipelineBranch,
421
+ conflictingFiles: conflictCheck.conflictingFiles,
422
+ preCheck: true,
452
423
  });
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);
424
+ }
425
+
426
+ // Use safeMerge instead of plain merge for better error handling
427
+ logger.info(`🔀 Merging task ${task.name} (${taskBranch}) into ${pipelineBranch}`);
428
+ const mergeResult = git.safeMerge(taskBranch, {
429
+ cwd: worktreeDir,
430
+ noFf: true,
431
+ message: `chore: merge task ${task.name} into pipeline`,
432
+ abortOnConflict: true,
433
+ });
434
+
435
+ if (!mergeResult.success) {
436
+ if (mergeResult.conflict) {
437
+ logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
438
+ state.status = 'failed';
439
+ state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
440
+ saveState(statePath, state);
441
+ process.exit(1);
469
442
  }
470
-
471
- git.push(pipelineBranch, { cwd: worktreeDir });
472
- } else {
473
- logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
443
+ throw new Error(mergeResult.error || 'Merge failed');
474
444
  }
475
445
 
446
+ // Log changed files
447
+ const stats = git.getLastOperationStats(worktreeDir);
448
+ if (stats) {
449
+ logger.info('Changed files:\n' + stats);
450
+ }
451
+
452
+ git.push(pipelineBranch, { cwd: worktreeDir });
453
+
476
454
  // Set previousTaskBranch for cleanup in the next iteration
477
- previousTaskBranch = noGit ? null : taskBranch;
455
+ previousTaskBranch = taskBranch;
478
456
  }
479
457
 
480
458
  // 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
- }
459
+ const flowBranch = laneName;
460
+ logger.section(`🏁 Final Consolidation: ${flowBranch}`);
461
+
462
+ // 1. Delete the very last task branch
463
+ if (previousTaskBranch) {
464
+ logger.info(`🧹 Deleting last task branch: ${previousTaskBranch}`);
465
+ try {
466
+ git.deleteBranch(previousTaskBranch, { cwd: worktreeDir, force: true });
467
+ } catch (e) {
468
+ logger.warn(` Failed to delete last task branch: ${e}`);
493
469
  }
470
+ }
494
471
 
495
- // 2. Create flow branch from pipelineBranch and cleanup
496
- if (flowBranch !== pipelineBranch) {
497
- logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
472
+ // 2. Create flow branch from pipelineBranch and cleanup
473
+ if (flowBranch !== pipelineBranch) {
474
+ logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
475
+ try {
476
+ // Create/Overwrite flow branch from pipeline branch
477
+ git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
478
+ git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
479
+
480
+ // 3. Delete temporary pipeline branch
481
+ logger.info(`🗑️ Deleting temporary pipeline branch: ${pipelineBranch}`);
482
+ // Must be on another branch to delete pipelineBranch
483
+ git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
484
+ git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
485
+
498
486
  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}`);
487
+ git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true, remote: true });
488
+ logger.info(` Deleted remote branch: origin/${pipelineBranch}`);
489
+ } catch {
490
+ // May not exist on remote or delete failed
519
491
  }
492
+
493
+ logger.success(`✓ Flow branch '${flowBranch}' is now the only remaining branch.`);
494
+ } catch (e) {
495
+ logger.error(`❌ Failed during final consolidation: ${e}`);
520
496
  }
521
497
  }
522
498
 
@@ -526,39 +502,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
526
502
  saveState(statePath, state);
527
503
 
528
504
  // 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
505
+ try {
506
+ // Always use current branch for comparison (already captured at start)
507
+ const finalStats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
508
+ if (finalStats) {
509
+ logger.info('Final Workspace Summary:\n' + finalStats);
561
510
  }
511
+ } catch (e) {
512
+ // Ignore
562
513
  }
563
514
 
564
515
  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`;
@@ -137,7 +137,6 @@ export async function runTask({
137
137
  chatId,
138
138
  runDir,
139
139
  runRoot,
140
- noGit = false,
141
140
  }: {
142
141
  task: Task;
143
142
  config: RunnerConfig;
@@ -148,7 +147,6 @@ export async function runTask({
148
147
  chatId: string;
149
148
  runDir: string;
150
149
  runRoot?: string;
151
- noGit?: boolean;
152
150
  }): Promise<TaskExecutionResult> {
153
151
  // Calculate runRoot if not provided (runDir is lanes/{laneName}/, runRoot is parent of lanes/)
154
152
  const calculatedRunRoot = runRoot || path.dirname(path.dirname(runDir));
@@ -158,11 +156,7 @@ export async function runTask({
158
156
 
159
157
  logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
160
158
  logger.info(`Model: ${model}`);
161
- if (noGit) {
162
- logger.info('🚫 noGit mode: skipping branch operations');
163
- } else {
164
- logger.info(`Branch: ${taskBranch}`);
165
- }
159
+ logger.info(`Branch: ${taskBranch}`);
166
160
 
167
161
  events.emit('task.started', {
168
162
  taskName: task.name,
@@ -170,32 +164,28 @@ export async function runTask({
170
164
  index,
171
165
  });
172
166
 
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
- }
167
+ // Sync pipelineBranch with remote before starting
168
+ logger.info(`🔄 Syncing ${pipelineBranch} with remote...`);
169
+
170
+ // Fetch latest from remote
171
+ try {
172
+ git.runGit(['fetch', 'origin', pipelineBranch], { cwd: worktreeDir, silent: true });
173
+ } catch {
174
+ // Branch might not exist on remote yet - that's OK
175
+ logger.info(` Branch ${pipelineBranch} not yet on remote, skipping sync`);
192
176
  }
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 });
177
+
178
+ // Try to fast-forward if behind
179
+ const syncResult = git.syncBranchWithRemote(pipelineBranch, { cwd: worktreeDir, createIfMissing: true });
180
+ if (syncResult.updated) {
181
+ logger.info(` ✓ Updated ${pipelineBranch} with ${syncResult.behind || 0} new commits from remote`);
182
+ } else if (syncResult.error) {
183
+ logger.warn(` ⚠️ Could not sync: ${syncResult.error}`);
198
184
  }
185
+
186
+ // Checkout task branch from pipeline branch
187
+ logger.info(`🌿 Forking task branch: ${taskBranch} from ${pipelineBranch}`);
188
+ git.runGit(['checkout', '-B', taskBranch, pipelineBranch], { cwd: worktreeDir });
199
189
 
200
190
  // Apply dependency permissions
201
191
  applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
@@ -208,8 +198,7 @@ export async function runTask({
208
198
 
209
199
  // Wrap prompt with context, dependency results, and completion instructions
210
200
  const wrappedPrompt = wrapPrompt(task.prompt, config, {
211
- noGit,
212
- isWorktree: !noGit,
201
+ isWorktree: true,
213
202
  dependencyResults,
214
203
  worktreePath: worktreeDir,
215
204
  taskBranch,
@@ -297,10 +286,8 @@ export async function runTask({
297
286
  }
298
287
  }
299
288
 
300
- // Push task branch (skip in noGit mode)
301
- if (!noGit) {
302
- git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
303
- }
289
+ // Push task branch
290
+ git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
304
291
 
305
292
  // Save task result for dependency handoff
306
293
  saveTaskResult(runDir, index, task.name, r1.resultText || '');
@@ -18,6 +18,7 @@ export * from './runner/index';
18
18
 
19
19
  // Import necessary parts for the CLI entry point
20
20
  import { runTasks } from './runner/pipeline';
21
+ import { cleanupAgentChildren } from './runner/agent';
21
22
 
22
23
  /**
23
24
  * CLI entry point
@@ -35,7 +36,6 @@ if (require.main === module) {
35
36
  const startIdxIdx = args.indexOf('--start-index');
36
37
  const pipelineBranchIdx = args.indexOf('--pipeline-branch');
37
38
  const worktreeDirIdx = args.indexOf('--worktree-dir');
38
- const noGit = args.includes('--no-git');
39
39
 
40
40
  const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
41
41
  const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
@@ -89,8 +89,19 @@ if (require.main === module) {
89
89
  // Add agent output format default
90
90
  config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'json';
91
91
 
92
+ // Handle process interruption to ensure cleanup
93
+ const handleSignal = (signal: string) => {
94
+ logger.warn(`\n⚠️ Runner received ${signal}. Shutting down...`);
95
+ // Cleanup any active agent child processes
96
+ cleanupAgentChildren();
97
+ process.exit(1);
98
+ };
99
+
100
+ process.on('SIGINT', () => handleSignal('SIGINT'));
101
+ process.on('SIGTERM', () => handleSignal('SIGTERM'));
102
+
92
103
  // Run tasks
93
- runTasks(tasksFile, config, runDir, { startIndex, noGit })
104
+ runTasks(tasksFile, config, runDir, { startIndex })
94
105
  .then(() => {
95
106
  process.exit(0);
96
107
  })