@litmers/cursorflow-orchestrator 0.1.20 → 0.1.26

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 (224) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +4 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +8 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +392 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +4 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +9 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1131 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +388 -162
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +374 -2
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
package/src/utils/git.ts CHANGED
@@ -195,6 +195,24 @@ export function getRepoRoot(cwd?: string): string {
195
195
  return runGit(['rev-parse', '--show-toplevel'], { cwd, silent: true });
196
196
  }
197
197
 
198
+ /**
199
+ * Get main repository root directory (for worktrees)
200
+ */
201
+ export function getMainRepoRoot(cwd?: string): string {
202
+ try {
203
+ const result = runGitResult(['worktree', 'list', '--porcelain'], { cwd });
204
+ if (result.success && result.stdout) {
205
+ const firstLine = result.stdout.split('\n')[0];
206
+ if (firstLine && firstLine.startsWith('worktree ')) {
207
+ return firstLine.slice(9).trim();
208
+ }
209
+ }
210
+ } catch {
211
+ // Fallback to normal repo root
212
+ }
213
+ return getRepoRoot(cwd);
214
+ }
215
+
198
216
  /**
199
217
  * Check if directory is a git repository
200
218
  */
@@ -217,8 +235,17 @@ export function worktreeExists(worktreePath: string, cwd?: string): boolean {
217
235
  * Create worktree
218
236
  */
219
237
  export function createWorktree(worktreePath: string, branchName: string, options: { cwd?: string; baseBranch?: string } = {}): string {
220
- const { cwd, baseBranch = 'main' } = options;
238
+ let { cwd, baseBranch } = options;
221
239
 
240
+ if (!baseBranch) {
241
+ baseBranch = getCurrentBranch(cwd) || 'refs/heads/main';
242
+ }
243
+
244
+ // Ensure baseBranch is unambiguous (branch name rather than tag)
245
+ const unambiguousBase = (baseBranch.startsWith('refs/') || baseBranch.includes('/'))
246
+ ? baseBranch
247
+ : `refs/heads/${baseBranch}`;
248
+
222
249
  // Use a file-based lock to prevent race conditions during worktree creation
223
250
  const lockDir = safeJoin(cwd || getRepoRoot(), '_cursorflow', 'locks');
224
251
  if (!fs.existsSync(lockDir)) {
@@ -255,7 +282,7 @@ export function createWorktree(worktreePath: string, branchName: string, options
255
282
  runGit(['worktree', 'add', worktreePath, branchName], { cwd });
256
283
  } else {
257
284
  // Create new branch from base
258
- runGit(['worktree', 'add', '-b', branchName, worktreePath, baseBranch], { cwd });
285
+ runGit(['worktree', 'add', '-b', branchName, worktreePath, unambiguousBase], { cwd });
259
286
  }
260
287
 
261
288
  return worktreePath;
@@ -497,3 +524,348 @@ export function getLastOperationStats(cwd?: string): string {
497
524
  return '';
498
525
  }
499
526
  }
527
+
528
+ // ============================================================================
529
+ // Enhanced Git Functions for Robustness
530
+ // ============================================================================
531
+
532
+ /**
533
+ * Generate a unique branch name that doesn't conflict with existing branches
534
+ */
535
+ export function generateUniqueBranchName(baseName: string, options: { cwd?: string; maxAttempts?: number } = {}): string {
536
+ const { cwd, maxAttempts = 10 } = options;
537
+ const timestamp = Date.now().toString(36);
538
+ const random = () => Math.random().toString(36).substring(2, 5);
539
+
540
+ // First attempt: base name with timestamp
541
+ let candidate = `${baseName}-${timestamp}-${random()}`;
542
+
543
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
544
+ if (!branchExists(candidate, { cwd })) {
545
+ return candidate;
546
+ }
547
+ // Try with new random suffix
548
+ candidate = `${baseName}-${timestamp}-${random()}`;
549
+ }
550
+
551
+ // Last resort: use full timestamp with random
552
+ return `${baseName}-${Date.now()}-${random()}`;
553
+ }
554
+
555
+ /**
556
+ * Safe merge result
557
+ */
558
+ export interface SafeMergeResult {
559
+ success: boolean;
560
+ conflict: boolean;
561
+ conflictingFiles: string[];
562
+ error?: string;
563
+ aborted: boolean;
564
+ }
565
+
566
+ /**
567
+ * Safely merge a branch with conflict detection and auto-abort
568
+ */
569
+ export function safeMerge(branchName: string, options: {
570
+ cwd?: string;
571
+ noFf?: boolean;
572
+ message?: string | null;
573
+ abortOnConflict?: boolean;
574
+ strategy?: 'ours' | 'theirs' | null;
575
+ } = {}): SafeMergeResult {
576
+ const { cwd, noFf = false, message = null, abortOnConflict = true, strategy = null } = options;
577
+
578
+ const args = ['merge'];
579
+
580
+ if (noFf) {
581
+ args.push('--no-ff');
582
+ }
583
+
584
+ if (strategy) {
585
+ args.push('-X', strategy);
586
+ }
587
+
588
+ if (message) {
589
+ args.push('-m', message);
590
+ }
591
+
592
+ args.push(branchName);
593
+
594
+ const result = runGitResult(args, { cwd });
595
+
596
+ if (result.success) {
597
+ return {
598
+ success: true,
599
+ conflict: false,
600
+ conflictingFiles: [],
601
+ aborted: false,
602
+ };
603
+ }
604
+
605
+ // Check for conflicts
606
+ const output = result.stdout + result.stderr;
607
+ const isConflict = output.includes('CONFLICT') || output.includes('Automatic merge failed');
608
+
609
+ if (isConflict) {
610
+ // Get conflicting files
611
+ const conflictingFiles = getConflictingFiles(cwd);
612
+
613
+ if (abortOnConflict) {
614
+ // Abort the merge
615
+ runGitResult(['merge', '--abort'], { cwd });
616
+
617
+ return {
618
+ success: false,
619
+ conflict: true,
620
+ conflictingFiles,
621
+ error: 'Merge conflict detected and aborted',
622
+ aborted: true,
623
+ };
624
+ }
625
+
626
+ return {
627
+ success: false,
628
+ conflict: true,
629
+ conflictingFiles,
630
+ error: 'Merge conflict - manual resolution required',
631
+ aborted: false,
632
+ };
633
+ }
634
+
635
+ return {
636
+ success: false,
637
+ conflict: false,
638
+ conflictingFiles: [],
639
+ error: result.stderr || 'Merge failed',
640
+ aborted: false,
641
+ };
642
+ }
643
+
644
+ /**
645
+ * Get list of conflicting files
646
+ */
647
+ export function getConflictingFiles(cwd?: string): string[] {
648
+ const result = runGitResult(['diff', '--name-only', '--diff-filter=U'], { cwd });
649
+ if (!result.success) return [];
650
+
651
+ return result.stdout.split('\n').filter(f => f.trim());
652
+ }
653
+
654
+ /**
655
+ * Check if merge is in progress
656
+ */
657
+ export function isMergeInProgress(cwd?: string): boolean {
658
+ const repoRoot = getRepoRoot(cwd);
659
+ return fs.existsSync(path.join(repoRoot, '.git', 'MERGE_HEAD'));
660
+ }
661
+
662
+ /**
663
+ * Abort ongoing merge
664
+ */
665
+ export function abortMerge(cwd?: string): boolean {
666
+ const result = runGitResult(['merge', '--abort'], { cwd });
667
+ return result.success;
668
+ }
669
+
670
+ /**
671
+ * Get HEAD commit hash
672
+ */
673
+ export function getHead(cwd?: string): string {
674
+ return runGit(['rev-parse', 'HEAD'], { cwd, silent: true });
675
+ }
676
+
677
+ /**
678
+ * Get short HEAD commit hash
679
+ */
680
+ export function getHeadShort(cwd?: string): string {
681
+ return runGit(['rev-parse', '--short', 'HEAD'], { cwd, silent: true });
682
+ }
683
+
684
+ /**
685
+ * Stash changes with optional message
686
+ */
687
+ export function stash(message?: string, options: { cwd?: string } = {}): boolean {
688
+ const args = ['stash', 'push'];
689
+ if (message) {
690
+ args.push('-m', message);
691
+ }
692
+
693
+ const result = runGitResult(args, { cwd: options.cwd });
694
+ return result.success;
695
+ }
696
+
697
+ /**
698
+ * Pop stashed changes
699
+ */
700
+ export function stashPop(options: { cwd?: string } = {}): boolean {
701
+ const result = runGitResult(['stash', 'pop'], { cwd: options.cwd });
702
+ return result.success;
703
+ }
704
+
705
+ /**
706
+ * Clean worktree (remove untracked files)
707
+ */
708
+ export function cleanWorktree(options: { cwd?: string; force?: boolean; directories?: boolean } = {}): void {
709
+ const args = ['clean'];
710
+ if (options.force) args.push('-f');
711
+ if (options.directories) args.push('-d');
712
+
713
+ runGit(args, { cwd: options.cwd });
714
+ }
715
+
716
+ /**
717
+ * Reset worktree to specific commit/branch
718
+ */
719
+ export function reset(target: string, options: { cwd?: string; mode?: 'soft' | 'mixed' | 'hard' } = {}): void {
720
+ const args = ['reset'];
721
+ if (options.mode) args.push(`--${options.mode}`);
722
+ args.push(target);
723
+
724
+ runGit(args, { cwd: options.cwd });
725
+ }
726
+
727
+ /**
728
+ * Checkout specific commit or branch
729
+ */
730
+ export function checkout(target: string, options: { cwd?: string; force?: boolean; createBranch?: boolean } = {}): void {
731
+ const args = ['checkout'];
732
+ if (options.force) args.push('-f');
733
+ if (options.createBranch) args.push('-b');
734
+ args.push(target);
735
+
736
+ runGit(args, { cwd: options.cwd });
737
+ }
738
+
739
+ /**
740
+ * Get commits between two refs
741
+ */
742
+ export function getCommitsBetween(fromRef: string, toRef: string, options: { cwd?: string } = {}): CommitInfo[] {
743
+ const format = '%H|%h|%an|%ae|%at|%s';
744
+ const result = runGitResult(['log', '--format=' + format, `${fromRef}..${toRef}`], { cwd: options.cwd });
745
+
746
+ if (!result.success) return [];
747
+
748
+ return result.stdout.split('\n')
749
+ .filter(line => line.trim())
750
+ .map(line => {
751
+ const parts = line.split('|');
752
+ return {
753
+ hash: parts[0] || '',
754
+ shortHash: parts[1] || '',
755
+ author: parts[2] || '',
756
+ authorEmail: parts[3] || '',
757
+ timestamp: parseInt(parts[4] || '0'),
758
+ subject: parts[5] || '',
759
+ };
760
+ });
761
+ }
762
+
763
+ /**
764
+ * Enhanced worktree creation with async lock
765
+ */
766
+ export async function createWorktreeAsync(
767
+ worktreePath: string,
768
+ branchName: string,
769
+ options: { cwd?: string; baseBranch?: string; timeout?: number } = {}
770
+ ): Promise<string> {
771
+ let { cwd, baseBranch, timeout = 30000 } = options;
772
+
773
+ if (!baseBranch) {
774
+ baseBranch = getCurrentBranch(cwd) || 'refs/heads/main';
775
+ }
776
+
777
+ // Ensure baseBranch is unambiguous
778
+ const unambiguousBase = (baseBranch.startsWith('refs/') || baseBranch.includes('/'))
779
+ ? baseBranch
780
+ : `refs/heads/${baseBranch}`;
781
+
782
+ const { acquireLock, releaseLock } = await import('./lock');
783
+ const lockDir = safeJoin(cwd || getRepoRoot(), '_cursorflow', 'locks');
784
+ if (!fs.existsSync(lockDir)) {
785
+ fs.mkdirSync(lockDir, { recursive: true });
786
+ }
787
+ const lockFile = safeJoin(lockDir, 'worktree.lock');
788
+
789
+ const acquired = await acquireLock(lockFile, {
790
+ timeoutMs: timeout,
791
+ operation: `createWorktree:${branchName}`,
792
+ });
793
+
794
+ if (!acquired) {
795
+ throw new Error('Failed to acquire worktree lock after timeout');
796
+ }
797
+
798
+ try {
799
+ // Check if branch already exists
800
+ const branchExistsLocal = runGitResult(['rev-parse', '--verify', branchName], { cwd }).success;
801
+
802
+ if (branchExistsLocal) {
803
+ runGit(['worktree', 'add', worktreePath, branchName], { cwd });
804
+ } else {
805
+ runGit(['worktree', 'add', '-b', branchName, worktreePath, unambiguousBase], { cwd });
806
+ }
807
+
808
+ return worktreePath;
809
+ } finally {
810
+ await releaseLock(lockFile);
811
+ }
812
+ }
813
+
814
+ /**
815
+ * Prune orphaned worktrees
816
+ */
817
+ export function pruneWorktrees(options: { cwd?: string } = {}): void {
818
+ runGit(['worktree', 'prune'], { cwd: options.cwd });
819
+ }
820
+
821
+ /**
822
+ * Get worktree for a specific path
823
+ */
824
+ export function getWorktreeForPath(targetPath: string, cwd?: string): WorktreeInfo | null {
825
+ const worktrees = listWorktrees(cwd);
826
+ return worktrees.find(wt => wt.path === targetPath) || null;
827
+ }
828
+
829
+ /**
830
+ * Sync branch with remote (fetch + merge or rebase)
831
+ */
832
+ export function syncWithRemote(branch: string, options: {
833
+ cwd?: string;
834
+ strategy?: 'merge' | 'rebase';
835
+ createIfMissing?: boolean;
836
+ } = {}): { success: boolean; error?: string } {
837
+ const { cwd, strategy = 'merge', createIfMissing = false } = options;
838
+
839
+ // Fetch the branch
840
+ const fetchResult = runGitResult(['fetch', 'origin', branch], { cwd });
841
+
842
+ if (!fetchResult.success) {
843
+ if (createIfMissing && fetchResult.stderr.includes('not found')) {
844
+ // Branch doesn't exist on remote, nothing to sync
845
+ return { success: true };
846
+ }
847
+ return { success: false, error: fetchResult.stderr };
848
+ }
849
+
850
+ // Merge or rebase
851
+ if (strategy === 'rebase') {
852
+ const result = runGitResult(['rebase', `origin/${branch}`], { cwd });
853
+ if (!result.success) {
854
+ // Abort rebase on failure
855
+ runGitResult(['rebase', '--abort'], { cwd });
856
+ return { success: false, error: result.stderr };
857
+ }
858
+ } else {
859
+ const mergeResult = safeMerge(`origin/${branch}`, {
860
+ cwd,
861
+ message: `chore: sync with origin/${branch}`,
862
+ abortOnConflict: true,
863
+ });
864
+
865
+ if (!mergeResult.success) {
866
+ return { success: false, error: mergeResult.error };
867
+ }
868
+ }
869
+
870
+ return { success: true };
871
+ }