{"record":{"id":"ccaa063c089f0072","repo":"paperclipai/paperclip","slug":"project-repository-is-not-a-git-checkout-relative","errorCode":null,"errorMessage":"Project repository is not a Git checkout: ${relative}","messagePattern":"Project repository is not a Git checkout: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/git-workspace-sync.ts","lineNumber":157,"sourceCode":"  }\n  return await runLocalGit(localDir, args, options);\n}\n\nexport async function readGitWorkspaceSnapshot(localDir: string, includeRepositories = true): Promise<GitWorkspaceSnapshot | null> {\n  const repositories: NonNullable<GitWorkspaceSnapshot[\"repositories\"]> = [];\n  if (includeRepositories) {\n    const root = path.join(localDir, PROJECT_REPOSITORIES_DIR);\n    const rootStat = await fs.lstat(root).catch((error: NodeJS.ErrnoException) => {\n      if (error.code === \"ENOENT\") return null;\n      throw error;\n    });\n    if (rootStat) {\n      if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error(\"Invalid project repositories directory\");\n      for (const entry of (await fs.readdir(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {\n        if (!entry.isDirectory() || !/^[a-zA-Z0-9_-]+$/.test(entry.name)) throw new Error(\"Invalid project repository directory\");\n        const relative = `${PROJECT_REPOSITORIES_DIR}/${entry.name}`;\n        const snapshot = await readGitWorkspaceSnapshot(path.join(localDir, relative), false);\n        if (!snapshot) throw new Error(`Project repository is not a Git checkout: ${relative}`);\n        repositories.push({ path: relative, snapshot });\n      }\n    }\n  }\n  // Only repository discovery may report an ordinary directory. A failed\n  // snapshot of a confirmed repository must never fall back to directory sync.\n  let insideWorkTree: GitCommandResult;\n  try {\n    insideWorkTree = await runLocalGit(localDir, [\"rev-parse\", \"--is-inside-work-tree\"], {\n      timeout: 10_000,\n      maxBuffer: 16 * 1024,\n    });\n  } catch (error) {\n    if (repositories.length === 0 && isNotAGitRepositoryError(error)) return null;\n    throw error;\n  }\n  if (insideWorkTree.stdout.trim() !== \"true\") {\n    return null;","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/adapter-utils/src/git-workspace-sync.ts#L139-L175","documentation":"For each valid-looking directory under `project-repositories/`, the library recurses with includeRepositories=false to confirm it is a real Git checkout. If the recursive snapshot returns null — the directory is not inside a Git work tree at its own top level — this error names the offending relative path, because repository discovery must never silently fall back to directory sync for a confirmed repository entry.","triggerScenarios":"An entry directory under `project-repositories/` passes the name/kind check but contains no `.git` (empty dir, partially failed clone, `.git` deleted, or a bare repo whose toplevel does not match), so readGitWorkspaceSnapshot(..., false) yields null.","commonSituations":"A clone was interrupted by disk-full or Ctrl-C leaving no `.git`; someone ran `rm -rf repo/.git`; the directory holds a `git init --bare` repo or just an extracted source tarball; a filesystem sync (Dropbox-style) stripped `.git`.","solutions":["Inspect the named path (`<localDir>/project-repositories/<name>`): run `git -C <path> rev-parse --is-inside-work-tree` to confirm it is not a checkout.","Re-clone or restore the repository into that directory so it has a working `.git` at its top level.","If the directory is junk (extracted tarball, empty leftovers), remove it and let workspace sync re-create it.","Avoid bare repos or nested submodules-as-toplevel here; each entry must itself be a standard non-bare work tree."],"exampleFix":"// before\nproject-repositories/api/        # extracted tarball, no .git\n// after\nrm -rf project-repositories/api\ngit clone git@github.com:org/api.git project-repositories/api","handlingStrategy":"validation","validationCode":"for (const dir of repositoryDirs) {\n  const r = await runGit(dir, [\"rev-parse\", \"--is-inside-work-tree\"]);\n  if (r.stdout.trim() !== \"true\") throw new Error(`Not a git checkout: ${dir}`);\n}","typeGuard":"null","tryCatchPattern":"try {\n  await snapshot(localDir);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Project repository is not a Git checkout:\")) {\n    const repoPath = err.message.split(\": \")[1];\n    // re-clone or remove the named repository, then retry\n  } else throw err;\n}","preventionTips":["Only populate project-repositories via real `git clone` (non-bare) into the entry directory itself.","Avoid extracting tarballs or copying source trees into the repositories dir.","Protect .git from cleanup scripts and file-sync tools that prune dotfiles.","After any interrupted clone, verify `git rev-parse --is-inside-work-tree` before snapshotting."],"tags":["git","filesystem","workspace-sync","validation"],"backgroundTag":"file-not-found","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}