ruvnet/ruflo · error · Error
refusing to prepare writing worktrees from a dirty repositor
Error message
refusing to prepare writing worktrees from a dirty repository
What it means
prepare() refuses to create writing worktrees when `git status --porcelain` in repoRoot reports anything and options.allowDirty is not true. Writing worktrees branch from committed HEAD, so uncommitted state would silently diverge between the main checkout and agent worktrees; the coordinator fails fast instead of proceeding from an inexact source state.
Source
Thrown at v3/@claude-flow/codex/src/worktrees/coordinator.ts:63
}
prepare(
runId: string,
agents: Array<{ id: string; readOnly?: boolean }>,
options: { allowDirty?: boolean; baseRef?: string } = {},
): WorktreeRunRecord {
assertId(runId, 'run id');
if (!agents.length) throw new Error('at least one agent is required');
const unique = new Set<string>();
for (const agent of agents) {
assertId(agent.id, 'agent id');
if (unique.has(agent.id)) throw new Error(`duplicate agent id: ${agent.id}`);
unique.add(agent.id);
}
const registryPath = this.registryPath(runId);
if (existsSync(registryPath)) return this.status(runId);
if (!options.allowDirty && git(this.repoRoot, ['status', '--porcelain']).length > 0) {
throw new Error('refusing to prepare writing worktrees from a dirty repository');
}
mkdirSync(this.registryDir, { recursive: true });
mkdirSync(join(this.worktreeBase, runId), { recursive: true });
const assignments: WorktreeAssignment[] = [];
try {
for (const agent of agents) {
const readOnly = agent.readOnly === true;
const branch = `ruflo/${runId}/${agent.id}`;
const worktreePath = join(this.worktreeBase, runId, agent.id);
if (readOnly) {
git(this.repoRoot, ['worktree', 'add', '--detach', worktreePath, options.baseRef ?? 'HEAD']);
} else {
git(this.repoRoot, ['worktree', 'add', '-b', branch, worktreePath, options.baseRef ?? 'HEAD']);
}
assignments.push({ agentId: agent.id, branch: readOnly ? '' : branch, path: worktreePath, readOnly });
}
const record: WorktreeRunRecord = {View on GitHub (pinned to fa13ee4ad6)
Solutions
- Commit or stash the changes (git stash, or git add + git commit) so `git status --porcelain` is empty, then re-run prepare()
- Add .gitignore entries for generated artifacts and clean untracked files (preview with `git clean -nd`)
- If a dirty base is acceptable, call prepare(runId, agents, { allowDirty: true }) — agent worktrees will then branch from HEAD without the uncommitted changes
Example fix
// before
coordinator.prepare('run-1', agents); // throws: dirty repository
// after
execSync('git stash --include-untracked', { cwd: repoRoot });
coordinator.prepare('run-1', agents); Defensive patterns
Strategy: validation
Validate before calling
import { execFileSync } from 'node:child_process';
function isClean(repoRoot: string): boolean {
return execFileSync('git', ['-C', repoRoot, 'status', '--porcelain'], { encoding: 'utf8' }).trim().length === 0;
}
if (!isClean(repoRoot) && !allowDirty) {
throw new Error('repo is dirty — commit, stash, or pass allowDirty');
}
coordinator.prepare(runId, agents, { allowDirty }); Try / catch
try {
coordinator.prepare(runId, agents);
} catch (e) {
if (e instanceof Error && e.message.includes('dirty repository')) {
// stash and retry once, or surface to the operator
} else throw e;
} Prevention
- Run prepare() only from a clean checkout; commit agent work before starting the next run
- Gitignore generated artifacts so untracked files do not count as dirty
When it happens
Trigger: prepare(runId, agents) with default options while the repository has staged, modified, or untracked files. A single untracked file that is not gitignored is enough to trigger it.
Common situations: Local development with leftover edits; a previous agent wrote into the main checkout instead of its worktree; CI jobs that generate files (dist/, coverage/, logs) without gitignore entries.
Related errors
- repoRoot must be the git top-level: ${top}
- duplicate agent id: ${agent.id}
- unknown worktree run: ${runId}
- registry path escapes owned worktree root: ${actual}
- Issue ${input.issueId} is not stealable or not claimed
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/cf83dbf22269b146.
Report an issue: GitHub.