{"record":{"id":"9af244b872bbfe14","repo":"paperclipai/paperclip","slug":"circular-workspace-dependency-while-staging-pack","errorCode":null,"errorMessage":"Circular workspace dependency while staging ${packageName}.","messagePattern":"Circular workspace dependency while staging (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/commands/install.ts","lineNumber":65,"sourceCode":"    const stderr = error && typeof error === \"object\" && \"stderr\" in error && typeof error.stderr === \"string\"\n      ? error.stderr.trim()\n      : \"\";\n    if (!stderr || (error instanceof Error && error.message.includes(stderr))) throw error;\n    throw new Error(`${error instanceof Error ? error.message : String(error)}\\n${stderr}`, { cause: error });\n  }\n}\n\nexport function resolveGitInstallWorkspacePackages(checkoutPath: string): ReleasePackageEntry[] {\n  const manifestPath = path.join(checkoutPath, \"scripts\", \"release-package-manifest.json\");\n  const manifest = JSON.parse(fs.readFileSync(manifestPath, \"utf8\")) as ReleasePackageEntry[];\n  const packageByName = new Map(manifest.map((entry) => [entry.name, entry]));\n  const visiting = new Set<string>();\n  const visited = new Set<string>();\n  const ordered: ReleasePackageEntry[] = [];\n\n  const visit = (packageName: string): void => {\n    if (visited.has(packageName)) return;\n    if (visiting.has(packageName)) throw new Error(`Circular workspace dependency while staging ${packageName}.`);\n    const entry = packageByName.get(packageName);\n    if (!entry) throw new Error(`Git install cannot stage workspace dependency ${packageName}; it is missing from scripts/release-package-manifest.json.`);\n    visiting.add(packageName);\n    const packageJson = JSON.parse(fs.readFileSync(path.join(checkoutPath, entry.dir, \"package.json\"), \"utf8\")) as Record<string, unknown>;\n    for (const section of [\"dependencies\", \"optionalDependencies\", \"peerDependencies\"] as const) {\n      const dependencies = packageJson[section];\n      if (!dependencies || typeof dependencies !== \"object\") continue;\n      for (const dependencyName of Object.keys(dependencies)) {\n        if (dependencyName.startsWith(\"@paperclipai/\")) visit(dependencyName);\n      }\n    }\n    visiting.delete(packageName);\n    visited.add(packageName);\n    ordered.push(entry);\n  };\n\n  visit(\"@paperclipai/server\");\n  return ordered;","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/commands/install.ts#L47-L83","documentation":"Thrown by resolveGitInstallWorkspacePackages (install.ts:65) when the depth-first visit over @paperclipai/* workspace dependencies re-enters a package already on the current visit stack. Detects a dependency cycle in the pnpm workspace graph during a git-based (source checkout) install.","triggerScenarios":"Two @paperclipai packages mutually depend on each other (A depends on B, B depends on A), or a longer cycle, while staging workspace packages in topological order. Triggered during `paperclipai install --ref ...` from a git checkout.","commonSituations":"A recent change added a circular workspace dependency; the manifest-driven installer cannot order such a graph.","solutions":["Break the cycle in the workspace: move shared code into a third package that both depend on.","If the cycle is in optionalDependencies/peerDependencies, re-evaluate whether it is truly needed.","Re-run the install after fixing the graph; verify with `pnpm why @paperclipai/<pkg>`."],"exampleFix":"// before: packages/a package.json\ndependencies: { \"@paperclipai/b\": \"workspace:*\" }\n// packages/b package.json\ndependencies: { \"@paperclipai/a\": \"workspace:*\" }\n// after: extract shared code into @paperclipai/shared and have both depend on it","handlingStrategy":"validation","validationCode":"function detectWorkspaceCycle(manifest: ReleasePackageEntry[], checkoutPath: string): string | null {\n  const byName = new Map(manifest.map((e) => [e.name, e]));\n  const visit = (name: string, stack: string[]): string[] | null => {\n    if (stack.includes(name)) return [...stack, name];\n    const entry = byName.get(name);\n    if (!entry) return null;\n    const pkg = JSON.parse(fs.readFileSync(path.join(checkoutPath, entry.dir, 'package.json'), 'utf8'));\n    for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) {\n      const deps = pkg[section];\n      if (!deps) continue;\n      for (const dep of Object.keys(deps)) {\n        if (dep.startsWith('@paperclipai/')) {\n          const cycle = visit(dep, [...stack, name]);\n          if (cycle) return cycle;\n        }\n      }\n    }\n    return null;\n  };\n  const cycle = visit('@paperclipai/server', []);\n  return cycle ? `Cycle: ${cycle.join(' -> ')}` : null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  const ordered = resolveGitInstallWorkspacePackages(checkoutPath);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Circular workspace dependency')) {\n    console.error('A workspace dependency cycle was introduced; break it before installing.');\n  }\n  throw err;\n}","preventionTips":["Keep the @paperclipai workspace graph acyclic; extract shared code into leaf packages.","Run a cycle-detection check in CI on the workspace manifest.","Review dependency direction before adding @paperclipai/* cross-references."],"tags":["cli","install","workspace","dependency-cycle"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}