ruvnet/ruflo · error

concurrent writers must use distinct worktrees: ${writerPath

Error message

concurrent writers must use distinct worktrees: ${writerPath}

What it means

After requiring isolated worktrees, the scheduler also verifies that no two workers in the same execution level resolve to the same directory: it collects path.resolve(worker.worktreePath ?? config.projectPath) for every non-readOnly worker in the level and throws when a path repeats. This catches two writers assigned the same worktree, or distinct worktreePath strings that resolve to one location (symlinks, duplicated segments).

Source

Thrown at v3/@claude-flow/codex/src/dual-mode/orchestrator.ts:443

      for (const worker of level) {
        placed.add(worker.id);
      }

      if (level.length > 0) {
        levels.push(level);
      }
    }

    for (const level of levels) {
      const writerPaths = new Set<string>();
      const writers = level.filter((item) => !item.readOnly);
      for (const worker of writers) {
        if (this.config.worktreeIsolation && !worker.worktreePath) {
          throw new Error(`writer ${worker.id} requires an isolated worktree`);
        }
        const writerPath = path.resolve(worker.worktreePath ?? this.config.projectPath);
        if (this.config.worktreeIsolation && writerPaths.has(writerPath)) {
          throw new Error(`concurrent writers must use distinct worktrees: ${writerPath}`);
        }
        writerPaths.add(writerPath);
      }
    }
    return levels;
  }

  /** Preserve read-only parallelism while independently bounding writers. */
  private partitionLevel(level: WorkerConfig[]): WorkerConfig[][] {
    const remaining = [...level];
    const batches: WorkerConfig[][] = [];
    while (remaining.length > 0) {
      const batch: WorkerConfig[] = [];
      let writers = 0;
      for (let index = 0; index < remaining.length && batch.length < this.config.maxConcurrent;) {
        const candidate = remaining[index]!;
        if (!candidate.readOnly && writers >= this.config.maxWriters) {
          index += 1;

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Give each writer its own unique worktreePath — parameterize the path by worker id when generating worktrees
  2. If both writers must touch the same checkout, serialize them: add a dependsOn edge so they land in different levels and never run concurrently
  3. If one of the two only reads, mark it readOnly: true so it is excluded from the writer-path set

Example fix

// before
const workers = [
  { id: 'coder-1', role: 'coder', worktreePath: '/repo/worktrees/shared' },
  { id: 'coder-2', role: 'coder', worktreePath: '/repo/worktrees/shared' }, // same level, same path
];

// after
const workers = ['coder-1', 'coder-2'].map((id) => ({
  id,
  role: 'coder',
  worktreePath: `/repo/worktrees/${id}`,
}));
Defensive patterns

Strategy: validation

Validate before calling

import path from 'node:path';
function assertDistinctWriterWorktrees(config: OrchestratorConfig, workers: WorkerConfig[]): void {
  const seen = new Set<string>();
  for (const w of workers.filter((x) => !x.readOnly)) {
    const p = path.resolve(w.worktreePath ?? config.projectPath);
    if (seen.has(p)) throw new Error(`two writers share worktree ${p}`);
    seen.add(p);
  }
}

Try / catch

Catch and treat as configuration: extract the duplicated path from the message, then either give each writer its own worktree or add a dependsOn edge to serialize them before re-running.

Prevention

When it happens

Trigger: Two non-readOnly workers with no dependsOn edge between them (same level) share an identical worktreePath, or their worktreePath values are different strings that resolve to the same directory, such as via symlinks or redundant ../ segments.

Common situations: Generating worktrees in a loop but assigning every worker index 0's path; pointing several writers at one shared checkout while worktreeIsolation is on; re-using a worktree template string without parameterizing it per worker.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/cc94cdcbb70560a6. Report an issue: GitHub.