Skip to main content
The merge queue is Longshot’s integration layer — it serializes parallel worker outputs into a single main branch, handling conflicts and ensuring the codebase stays coherent.

Queue Architecture

interface MergeQueueEntry {
  branch: string;      // Branch to merge (e.g., "worker/task-001-...")
  priority: number;    // 1 (highest) to 10 (lowest)
  enqueuedAt: number;  // Unix timestamp ms
}
Branches are dequeued in priority order, with ties broken by enqueue time (FIFO):
// From merge-queue.ts:139-144
this.queue.push({ branch, priority, enqueuedAt: Date.now() });
this.queue.sort((a, b) =>
  a.priority !== b.priority 
    ? a.priority - b.priority 
    : a.enqueuedAt - b.enqueuedAt
);
Foundational tasks (priority 1-2) merge before feature work (priority 3-5), which merges before polish (priority 8-10). This ensures dependencies land in the correct order.

Merge Strategies

Configurable per deployment:
export type MergeStrategy = "fast-forward" | "rebase" | "merge-commit";

Fast-Forward

Attempts a clean fast-forward merge. If it fails, falls back to merge-commit:
// From merge-queue.ts:278-287
let result = await this.mergeBranch(mergeRef, this.mainBranch, this.mergeStrategy);

if (!result.success && !result.conflicted && this.mergeStrategy !== "merge-commit") {
  logger.warn(`${this.mergeStrategy} failed, falling back to merge-commit`);
  await this.abortMerge();
  result = await this.mergeBranch(mergeRef, this.mainBranch, "merge-commit");
}

Rebase

Rebases the branch onto the latest main before merging. Keeps a linear history but rewrites commits.

Merge-Commit

Always creates a merge commit. Preserves full history but creates non-linear topology.
Recommendation: Use rebase for clean history and easier bisection. Fall back to merge-commit if rebase fails.

Conflict Handling

When a merge conflict occurs, the queue uses a retry-before-escalate strategy:

Retry Flow

// From merge-queue.ts:337-399
if (result.conflicted) {
  const retries = this.retryCount.get(branch) ?? 0;
  
  if (retries < this.maxConflictRetries) {
    // Rebase branch onto latest main before re-queuing
    const localBranch = `retry-rebase-${Date.now()}`;
    await execFileAsync("git", ["checkout", "-b", localBranch, `origin/${branch}`]);
    
    const rebaseResult = await this.rebaseBranch(localBranch, this.mainBranch);
    if (rebaseResult.success) {
      await execFileAsync("git", ["push", "origin", `${localBranch}:${branch}", "--force"]);
      logger.info("Rebased branch onto latest main before retry");
    }
    
    this.enqueue(branch, 1);  // High priority for retry
    this.retryCount.set(branch, retries + 1);
    
    return { success: false, status: "skipped", message: "Conflict retry" };
  }
  
  // Retries exhausted — escalate to planner
  for (const cb of this.conflictCallbacks) {
    cb({ branch, conflictingFiles: conflicts });
  }
}
Default: 2 retries before escalation. Many conflicts resolve automatically when the branch is rebased onto a newer main that includes related changes.

Why Rebase Before Retry?

A branch may conflict with main at time T but succeed after main advances to T+1:
Time T:     main has feature A
            task-001 conflicts with feature A
            
Time T+1:   main has feature A + feature B (which fixes A's interface)
            task-001 rebased onto new main → no conflict
Rebasing before retry gives the branch a fresh base that may already include resolution.

Conflict Escalation

When retries are exhausted, the orchestrator is notified:
// Orchestrator registers conflict handler
mergeQueue.onConflict((info: MergeConflictInfo) => {
  const fixTask: Task = {
    id: `conflict-${Date.now()}",
    description: `Resolve merge conflict in ${info.branch}`,
    scope: info.conflictingFiles,
    acceptance: "git merge completes without conflict markers",
    branch: `fix/${info.branch}`,
    status: "pending",
    priority: 1,
    conflictSourceBranch: info.branch,
  };
  
  planner.injectTask(fixTask);
});
The conflict-fix task is injected directly into the planner’s dispatch pipeline, bypassing the LLM planning cycle.
Conflict-fix tasks have conflictSourceBranch set. After they complete successfully, the original branch’s retry count is reset and it’s re-queued.

Background Processing

The merge queue runs in a background loop:
// From merge-queue.ts:163-196
startBackground(intervalMs: number = 5_000): void {
  this.backgroundRunning = true;
  
  const tick = async () => {
    try {
      while (this.queue.length > 0 && this.backgroundRunning) {
        const branch = this.dequeue();
        if (branch) {
          const result = await this.mergeBranch(branch);
          for (const cb of this.mergeResultCallbacks) {
            cb(result);
          }
        }
      }
    } catch (error) {
      logger.error("Background merge tick error", { error });
    }
    
    if (this.backgroundRunning) {
      setTimeout(() => tick(), intervalMs);
    }
  };
  
  setTimeout(() => tick(), intervalMs);
}
Merges are processed continuously as long as work exists in the queue.

Git Mutex

Merge operations are serialized to prevent concurrent git state corruption:
// From merge-queue.ts:246-248, 450-454
if (this.gitMutex) {
  await this.gitMutex.acquire();
}

try {
  // ... perform merge ...
} finally {
  if (this.gitMutex) {
    this.gitMutex.release();
  }
}
Only one merge executes at a time, even if multiple queue background loops exist.

Clean State Enforcement

Before and after each merge, the queue ensures the repository is in a clean state:
// From merge-queue.ts:476-521
private async ensureCleanState(mainBranch: string, cwd: string): Promise<void> {
  // Abort any in-progress merge/rebase
  await execFileAsync("git", ["rebase", "--abort"], { cwd }).catch(() => {});
  await execFileAsync("git", ["merge", "--abort"], { cwd }).catch(() => {});
  
  // Reset to HEAD
  await execFileAsync("git", ["reset", "--hard", "HEAD"], { cwd }).catch(() => {});
  
  // Clean untracked files
  await execFileAsync("git", ["clean", "-fd"], { cwd }).catch(() => {});
  
  // Delete temporary retry branches
  const { stdout } = await execFileAsync("git", ["branch", "--list", "retry-rebase-*"]);
  const branches = stdout.trim().split("\n").filter(Boolean);
  for (const branch of branches) {
    await execFileAsync("git", ["branch", "-D", branch], { cwd }).catch(() => {});
  }
  
  // Return to main branch
  await execFileAsync("git", ["checkout", mainBranch], { cwd }).catch(() => {});
}
All cleanup operations use .catch(() => {}) to be best-effort and never throw. A failed cleanup is logged but doesn’t halt the queue.

Merge Statistics

interface MergeStats {
  totalMerged: number;      // Successful merges
  totalSkipped: number;     // Retry-deferred merges
  totalFailed: number;      // Hard failures
  totalConflicts: number;   // Conflicts detected
}
Stats are tracked and exposed for monitoring:
const stats = mergeQueue.getMergeStats();
const successRate = Math.round(
  (stats.totalMerged / (stats.totalMerged + stats.totalConflicts)) * 100
);
The planner includes merge queue health in its context for adaptive planning:
// From planner.ts:556-567
const mergeStats = this.mergeQueue.getMergeStats();
msg += `## Merge Queue Health\n`;
msg += `Merged: ${mergeStats.totalMerged} | Conflicts: ${mergeStats.totalConflicts}\n`;
msg += `Queue depth: ${this.mergeQueue.getQueueLength()}\n`;
if (mergeStats.totalMerged + mergeStats.totalConflicts > 0) {
  const rate = Math.round(
    (mergeStats.totalMerged / (mergeStats.totalMerged + mergeStats.totalConflicts)) * 100
  );
  msg += `Success rate: ${rate}%\n`;
}
High conflict rates indicate overlapping task scopes. The planner can use this signal to avoid assigning concurrent work to the same files.

Remote Cleanup

After successful merge, the remote branch is deleted:
// From merge-queue.ts:302-310
try {
  await execFileAsync("git", ["push", "origin", "--delete", branch], { cwd });
  logger.debug(`Deleted remote branch ${branch}`);
} catch (error) {
  logger.debug(`Best-effort delete of remote branch ${branch} failed", {
    error: error.message,
  });
}
This keeps the repository clean and prevents branch proliferation.

Queue Observability

The merge queue exposes several observability methods:
getQueue(): string[]                    // Current queue contents
getQueueLength(): number                // Queue depth
isBranchMerged(branch: string): boolean // Check if already merged
getMergeStats(): MergeStats             // Lifetime statistics
These power the dashboard’s merge queue visualization and planner feedback.

Dependency Ordering Example

Consider three tasks:
const tasks = [
  { id: "task-001", priority: 1, description: "Create User type" },
  { id: "task-002", priority: 3, description: "Implement createUser()" },
  { id: "task-003", priority: 3, description: "Implement listUsers()" },
];
All three complete in parallel. The merge queue processes them:
  1. task-001 (priority 1) merges first → User type now in main
  2. task-002 and task-003 (both priority 3) merge in FIFO order
  3. Both use the User type that’s now available in main
No conflicts occur because the dependency (User type) landed before its consumers.
If task-002 and task-003 had the same priority as task-001, they might merge before task-001, causing type errors. Priority ordering prevents this.

Failure Recovery

If a merge fails catastrophically (not a conflict, but a git command error):
// From merge-queue.ts:435-449
catch (error) {
  this.stats.totalFailed++;
  const msg = error instanceof Error ? error.message : String(error);
  logger.error(`Error merging branch ${branch}`, { error: msg });
  
  // Restore clean state before returning
  await this.ensureCleanState(this.mainBranch, cwd);
  
  return { success: false, status: "failed", branch, message: msg };
}
The queue ensures the repository is left in a clean state even after errors, preventing cascading failures.

Conflict Marker Detection

After abort, the reconciler scans for leftover conflict markers:
git grep -rl "<<<<<<<" -- "*.ts" "*.tsx" "*.js" "*.json"
Any files with markers trigger high-priority fix tasks to manually resolve them.

Build docs developers (and LLMs) love