ruvnet/ruflo · error · Error
repoPath must be the Git top-level: ${top}
Error message
repoPath must be the Git top-level: ${top} What it means
captureRepositorySourceState() requires repoPath to be exactly the worktree top-level: it realpaths the input and compares it with `git rev-parse --show-toplevel` (also realpathed). Passing a subdirectory of the repo, or a path that resolves elsewhere, throws. This guarantees subsequent git plumbing always runs from the true root.
Source
Thrown at v3/@claude-flow/codex/src/harness/repository-state.ts:411
|| submodule.commit !== submodule.expectedCommit
|| submodule.trackedPatch.bytes > 0
|| submodule.untrackedManifest.entries.length > 0
|| submodule.submodules.some(hasDirtySubmodule);
}
/**
* Capture a stable, content-addressed Git source state.
*
* The collector performs two complete reads and refuses a moving worktree.
* Ignored files are intentionally excluded because Git does not treat them as
* repository source. Untracked symlinks bind their link target text without
* following the target outside the worktree.
*/
export function captureRepositorySourceState(repoPath: string | URL): ExactSourceState {
const requested = repoPath instanceof URL ? fileURLToPath(repoPath) : repoPath;
const repoRoot = realpathSync(resolve(requested));
const top = realpathSync(gitText(repoRoot, ['rev-parse', '--show-toplevel']));
if (top !== repoRoot) throw new Error(`repoPath must be the Git top-level: ${top}`);
const cleanStatus = (): Buffer => gitBuffer(repoRoot, [
'status',
'--porcelain=v1',
'-z',
'--untracked-files=all',
'--ignore-submodules=none',
]);
const initialStatus = cleanStatus();
if (initialStatus.length === 0) {
repositoryPaths(repoRoot, false);
const baseCommit = gitText(repoRoot, ['rev-parse', '--verify', 'HEAD']);
const treeId = gitText(repoRoot, ['rev-parse', '--verify', 'HEAD^{tree}']);
if (cleanStatus().length !== 0 || gitText(repoRoot, ['rev-parse', '--verify', 'HEAD']) !== baseCommit) {
throw new Error('repository changed while capturing exact source state');
}
const repository = repositoryIdentity(repoRoot);
const identity = {View on GitHub (pinned to fa13ee4ad6)
Solutions
- Resolve the root first: pass the output of `git rev-parse --show-toplevel`
- In CLI code, chdir or resolve upward from cwd before calling
- If you truly need a subdirectory scope, that is unsupported — capture the whole worktree
Example fix
// before
captureRepositorySourceState('v3/@claude-flow/codex'); // subdir -> throws
// after
import { execSync } from 'node:child_process';
const root = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
captureRepositorySourceState(root); Defensive patterns
Strategy: validation
Validate before calling
import { execSync } from 'node:child_process';
const gitTop = (p: string) => execSync('git rev-parse --show-toplevel', { cwd: p, encoding: 'utf8' }).trim();
if (realpathSync(resolve(p)) !== realpathSync(gitTop(p))) throw new Error('not top-level'); Prevention
- Centralize repo-root derivation (rev-parse --show-toplevel) in one helper and use it everywhere
- Test harness entry points from subdirectories to catch cwd assumptions
- Remember subdirectory scoping is unsupported: always capture the whole worktree
When it happens
Trigger: Calling captureRepositorySourceState('src/subdir') or with process.cwd() while the process runs inside a subdirectory; passing a linked worktree's inner directory; URL paths whose fileURLToPath result is not the top-level.
Common situations: CLIs invoke the harness from wherever the user happens to be (a monorepo package dir); scripts hardcode a package path; monorepo subpackages each try to snapshot 'their' directory.
Related errors
- repoRoot must be the git top-level: ${top}
- duplicate repository path: ${path}
- duplicate agent id: ${agent.id}
- localCompute: no adapter for graphId=${input.graphId}
- Invalid completion type
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/e9b3caa8c7a6902b.
Report an issue: GitHub.