{"record":{"id":"be75e67fe8a37b43","repo":"paperclipai/paperclip","slug":"attempt-source-is-not-a-real-directory-source","errorCode":null,"errorMessage":"Attempt source is not a real directory: ${source}","messagePattern":"Attempt source is not a real directory: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs","lineNumber":234,"sourceCode":"    maxParallelPerShard: Math.floor(maxParallel / 2),\n  };\n}\n\nasync function writeGithubOutput(entries) {\n  const output = process.env.GITHUB_OUTPUT;\n  if (!output) return;\n  await appendFile(\n    output,\n    Object.entries(entries)\n      .map(([key, value]) => `${key}=${value}\\n`)\n      .join(\"\"),\n  );\n}\n\nasync function copyAttempt(source, destination) {\n  const metadata = await lstat(source);\n  if (!metadata.isDirectory() || metadata.isSymbolicLink()) {\n    throw new Error(`Attempt source is not a real directory: ${source}`);\n  }\n  await mkdir(destination, { recursive: false, mode: 0o700 });\n  for (const entry of await readdir(source, { withFileTypes: true })) {\n    if (\n      !entry.isFile() ||\n      entry.isSymbolicLink() ||\n      !ATTEMPT_FILES.has(entry.name)\n    ) {\n      throw new Error(`Unexpected attempt path ${join(source, entry.name)}`);\n    }\n    await cp(join(source, entry.name), join(destination, entry.name), {\n      errorOnExist: true,\n    });\n  }\n  for (const required of [\n    \"artifact.json\",\n    \"score.json\",\n    \"case.json\",","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs#L216-L252","documentation":"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.","triggerScenarios":"Passing a path to copyAttempt that does not exist, is a regular file, or is a symbolic link rather than a real directory.","commonSituations":"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.","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)."],"exampleFix":"// before\ncopyAttempt(\"runs/latest\", dest); // latest is a symlink\n// after\ncopyAttempt(await fs.realpath(\"runs/latest\"), dest);","handlingStrategy":"validation","validationCode":"const st = await lstat(source);\nif (!st.isDirectory() || st.isSymbolicLink()) {\n  throw new Error(`Attempt source must be a real directory: ${source}`);\n}","typeGuard":"function isRealDirectoryStat(st) {\n  return st.isDirectory() && !st.isSymbolicLink();\n}","tryCatchPattern":"try {\n  await copyAttempt(source, dest);\n} catch (err) {\n  if (err.message.startsWith(\"Attempt source is not a real directory\")) {\n    console.error(`Fix or recreate the attempt at ${source}`);\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["Resolve symlinks with realpath before handing attempt paths to the copier.","Clean up partially-failed run directories instead of reusing them."],"tags":["filesystem","symlink","validation"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}