paperclipai/paperclip · error · Error
Attempt source is not a real directory: ${source}
Error message
Attempt source is not a real directory: ${source} What it means
copyAttempt copies a runner attempt directory into the campaign destination. It first lstats the source and requires it to be a real directory that is not a symlink. This guards against copying symlinks or files as attempts, which would break the per-attempt file enumeration that follows.
Source
Thrown at packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs:234
maxParallelPerShard: Math.floor(maxParallel / 2),
};
}
async function writeGithubOutput(entries) {
const output = process.env.GITHUB_OUTPUT;
if (!output) return;
await appendFile(
output,
Object.entries(entries)
.map(([key, value]) => `${key}=${value}\n`)
.join(""),
);
}
async function copyAttempt(source, destination) {
const metadata = await lstat(source);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
throw new Error(`Attempt source is not a real directory: ${source}`);
}
await mkdir(destination, { recursive: false, mode: 0o700 });
for (const entry of await readdir(source, { withFileTypes: true })) {
if (
!entry.isFile() ||
entry.isSymbolicLink() ||
!ATTEMPT_FILES.has(entry.name)
) {
throw new Error(`Unexpected attempt path ${join(source, entry.name)}`);
}
await cp(join(source, entry.name), join(destination, entry.name), {
errorOnExist: true,
});
}
for (const required of [
"artifact.json",
"score.json",
"case.json",View on GitHub (pinned to 01ad858492)
Solutions
- Verify the source attempt path exists and is an actual directory (ls -la).
- Recreate the attempt run if the directory was deleted or moved.
- Replace any symlink to the attempt directory with the real directory (or copy the real content).
Example fix
// before
copyAttempt("runs/latest", dest); // latest is a symlink
// after
copyAttempt(await fs.realpath("runs/latest"), dest); Defensive patterns
Strategy: validation
Validate before calling
const st = await lstat(source);
if (!st.isDirectory() || st.isSymbolicLink()) {
throw new Error(`Attempt source must be a real directory: ${source}`);
} Type guard
function isRealDirectoryStat(st) {
return st.isDirectory() && !st.isSymbolicLink();
} Try / catch
try {
await copyAttempt(source, dest);
} catch (err) {
if (err.message.startsWith("Attempt source is not a real directory")) {
console.error(`Fix or recreate the attempt at ${source}`);
process.exit(1);
}
throw err;
} Prevention
- Resolve symlinks with realpath before handing attempt paths to the copier.
- Clean up partially-failed run directories instead of reusing them.
When it happens
Trigger: Passing a path to copyAttempt that does not exist, is a regular file, or is a symbolic link rather than a real directory.
Common situations: A stale attempt path left after cleanup; the attempt directory being symlinked (common in workspace setups); passing a parent directory that was moved; a partially failed run that left a file where a directory was expected.
Related errors
- workspace_durable_seed_invalid
- Registered base project workspace Paperclip config at ${conf
- ${label} must be a canonical path and cannot use a symlink a
- ${label} is not a regular file at ${canonical}.
- codex auth cache: account_id is not a valid account handle
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/be75e67fe8a37b43.
Report an issue: GitHub.