Skip to main content

Overview

The MergeQueue manages serial integration of worker branches into the main branch. It implements priority-based ordering, automatic conflict retry with rebase, and health tracking. Location: packages/orchestrator/src/merge-queue.ts

Class: MergeQueue

Constructor

new MergeQueue(
  config: {
    mergeStrategy: MergeStrategy
    mainBranch: string
    repoPath: string
    gitMutex?: GitMutex
    maxConflictRetries?: number
  },
  deps?: Partial<MergeQueueDeps>
)
config.mergeStrategy
'fast-forward' | 'rebase' | 'merge-commit'
required
Primary merge strategy (default: ‘rebase’)
config.mainBranch
string
required
Target branch name (default: ‘main’)
config.repoPath
string
required
Local repository path
config.gitMutex
GitMutex
Optional mutex to serialize git operations
config.maxConflictRetries
number
default:"2"
Max retries before escalating conflict to fix task

Core Methods

enqueue()

Adds a branch to the merge queue.
enqueue(branch: string, priority: number = 5): void
branch
string
required
Branch name to merge (e.g., “worker/task-001-auth”)
priority
number
default:"5"
Lower = higher priority. Fix tasks use priority 1.
Deduplication:
  • Skips if branch already merged
  • Skips if branch already in queue
Sorting:
queue.sort((a, b) => 
  a.priority !== b.priority 
    ? a.priority - b.priority      // Primary: priority
    : a.enqueuedAt - b.enqueuedAt  // Secondary: FIFO
)

mergeBranch()

Attempts to merge a single branch.
async mergeBranch(branch: string): Promise<MergeQueueResult>
branch
string
required
Branch to merge
Returns: MergeQueueResult
interface MergeQueueResult {
  success: boolean
  status: 'merged' | 'skipped' | 'failed' | 'conflict'
  branch: string
  message: string
  conflicts?: string[]  // Conflicting file paths
}
Merge Flow:
  1. Acquire git mutex (if configured)
  2. Clean state: abort any in-progress rebase/merge, reset hard, checkout main
  3. Fetch branch: git fetch origin <branch>
  4. Checkout main: Ensure clean working tree
  5. Merge: git merge origin/<branch> with configured strategy
  6. Fallback: If rebase/fast-forward fails, retry with merge-commit
  7. Push: git push origin main
  8. Cleanup: Delete remote branch
  9. Release mutex
Conflict Handling: If merge results in conflict:
if (result.conflicted) {
  const retries = this.retryCount.get(branch) ?? 0
  
  if (retries < maxConflictRetries) {
    // Rebase branch onto latest main
    await rebaseBranch(branch, mainBranch)
    
    // Re-enqueue with highest priority
    this.enqueue(branch, 1)
    this.retryCount.set(branch, retries + 1)
    
    return { status: 'skipped', message: 'Conflict retry ...' }
  }
  
  // Retries exhausted → fire onConflict callback
  for (const cb of this.conflictCallbacks) {
    cb({ branch, conflictingFiles: conflicts })
  }
  
  return { status: 'conflict', conflicts }
}

processQueue()

Processes all queued branches sequentially.
async processQueue(): Promise<MergeQueueResult[]>
Used during finalization to drain the queue synchronously.

startBackground()

Starts background merge loop.
startBackground(intervalMs: number = 5_000): void
intervalMs
number
default:"5000"
Tick interval in milliseconds
Processes queue continuously until stopBackground() is called.

stopBackground()

stopBackground(): void
Stops the background merge loop.

Merge Strategies

Rebase (default)

mergeStrategy: 'rebase'
Git commands:
git fetch origin <branch>
git checkout main
git rebase origin/<branch>
git push origin main
Benefits: Linear history, no merge commits

Fast-Forward

mergeStrategy: 'fast-forward'
Git commands:
git fetch origin <branch>
git checkout main
git merge --ff-only origin/<branch>
git push origin main
Requirement: Branch must be direct descendant of main

Merge Commit

mergeStrategy: 'merge-commit'
Git commands:
git fetch origin <branch>
git checkout main
git merge --no-ff origin/<branch>
git push origin main
Result: Creates merge commit even if fast-forward is possible

Conflict Retry with Rebase

Before re-queueing a conflicting branch:
try {
  // Create local tracking branch
  await git(['checkout', '-b', localBranch, `origin/${branch}`])
  
  // Rebase onto latest main
  const rebaseResult = await rebaseBranch(localBranch, mainBranch)
  
  if (rebaseResult.success) {
    // Force-push rebased branch
    await git(['push', 'origin', `${localBranch}:${branch}`, '--force'])
    logger.info('Rebased branch onto latest main before retry')
  }
  
  // Cleanup local branch
  await git(['checkout', mainBranch])
  await git(['branch', '-D', localBranch])
} catch (error) {
  // Best-effort — restore clean state and continue
  await ensureCleanState(mainBranch)
}
This ensures the next merge attempt works against current HEAD rather than a stale base.

Statistics

getMergeStats()

getMergeStats(): MergeStats
Returns:
interface MergeStats {
  totalMerged: number      // Successful merges
  totalSkipped: number     // Skipped (already merged, etc.)
  totalFailed: number      // Failed merges
  totalConflicts: number   // Conflicts (after retries exhausted)
}

isBranchMerged()

isBranchMerged(branch: string): boolean
Checks if branch has been successfully merged.

getQueueLength()

getQueueLength(): number
Returns current queue depth.

resetRetryCount()

resetRetryCount(branch: string): void
Resets retry counter for a branch. Used during finalization.

Callbacks

onMergeResult()

onMergeResult(callback: (result: MergeQueueResult) => void): void
Called after each merge attempt.

onConflict()

onConflict(callback: (info: MergeConflictInfo) => void): void
Conflict Info:
interface MergeConflictInfo {
  branch: string
  conflictingFiles: string[]
}
Typically triggers creation of a conflict-resolution fix task.

State Management

ensureCleanState()

Nuclear git cleanup — best-effort, never throws.
private async ensureCleanState(mainBranch: string): Promise<void>
Operations:
  1. git rebase --abort (best-effort)
  2. git merge --abort (best-effort)
  3. git reset --hard HEAD
  4. git clean -fd
  5. Delete all retry-rebase-* temp branches
  6. git checkout <mainBranch>
Called before each merge and on errors to prevent git state corruption.

Usage Example

import { MergeQueue } from '@longshot/orchestrator'

const mergeQueue = new MergeQueue({
  mergeStrategy: 'rebase',
  mainBranch: 'main',
  repoPath: './target-repo',
  maxConflictRetries: 2
})

// Register callbacks
mergeQueue.onMergeResult((result) => {
  console.log(`Merge ${result.branch}: ${result.status}`)
  if (result.status === 'conflict') {
    console.log(`Conflicts: ${result.conflicts?.join(', ')}`)
  }
})

mergeQueue.onConflict(({ branch, conflictingFiles }) => {
  console.log(`Creating fix task for ${branch}`)
  console.log(`Conflicting files: ${conflictingFiles.join(', ')}`)
  // Create and inject conflict-resolution task
})

// Enqueue branches
mergeQueue.enqueue('worker/task-001-auth', 5)
mergeQueue.enqueue('worker/task-002-api', 3)

// Start background processing
mergeQueue.startBackground(5_000)

// Or process synchronously
const results = await mergeQueue.processQueue()
console.log(`Merged ${results.filter(r => r.success).length} branches`)

// Get statistics
const stats = mergeQueue.getMergeStats()
console.log(`Success rate: ${stats.totalMerged}/${stats.totalMerged + stats.totalConflicts}`)

Dependency Injection

interface MergeQueueDeps {
  execFileAsync: typeof execFileAsync
  checkoutBranch: typeof checkoutBranch
  mergeBranch: typeof mergeBranch
  rebaseBranch: typeof rebaseBranch
  now: () => number
  setTimeout: typeof setTimeout
  clearTimeout: typeof clearTimeout
}
Allows deterministic testing with in-memory git operations.

Build docs developers (and LLMs) love