EveryInc/compound-engineering-plugin · warning

Skipping ${targetDir}: existing user-managed symlink (not ov

Error message

Skipping ${targetDir}: existing user-managed symlink (not overwritten)

What it means

cleanupCurrentManagedSkillDir in src/targets/pi.ts is the preserve-check for Pi managed skill directories. If the target directory exists as a symlink, it is treated as user-managed and skipped (returns true) — cleanup and install will never follow or overwrite it. This mirrors the shared symlink-preservation policy across target writers.

Source

Thrown at src/targets/pi.ts:462

    if (!isSafeManagedPath(agentsDir, agentFile)) continue
    const targetPath = path.join(agentsDir, agentFile)
    if (await isPreservedSymlink(targetPath)) continue
    await fs.rm(targetPath, { force: true })
  }
}

// Returns true when the existing path was preserved (skip cleanup AND the
// subsequent copy/write -- writing through a preserved symlink would clobber
// the user's fork, which is worse than not cleaning up at all).
async function cleanupCurrentManagedSkillDir(
  targetDir: string,
  manifest: PiInstallManifest | null,
  skillName: string,
): Promise<boolean> {
  const stat = await lstatOrNull(targetDir)
  if (!stat) return false
  if (stat.isSymbolicLink()) {
    console.warn(`Skipping ${targetDir}: existing user-managed symlink (not overwritten)`)
    return true
  }
  if (!manifest?.skills.includes(skillName)) {
    console.warn(`Skipping ${targetDir}: existing unmanaged directory (not overwritten)`)
    return true
  }
  await fs.rm(targetDir, { recursive: true, force: true })
  return false
}

async function cleanupCurrentManagedAgentFile(
  targetPath: string,
  manifest: PiInstallManifest | null,
  agentFileName: string,
): Promise<boolean> {
  const stat = await lstatOrNull(targetPath)
  if (!stat) return false
  if (stat.isSymbolicLink()) {

View on GitHub (pinned to c9c10f8c75)

Solutions

  1. If the symlink is intentional, nothing to do — the directory is preserved.
  2. Remove the symlink (rm the link, not its target) and re-run the install to get the managed skill directory written.
  3. If the link target has customizations, reconcile them into your own copy or the plugin source before removing the link.
  4. Confirm with `ls -la <managed skills root>` which skill entries are symlinks before re-running.

Example fix

// before
ls -la ~/.config/pi/skills/ce-plan  # -> ~/dotfiles/pi-skills/ce-plan
// after
rm ~/.config/pi/skills/ce-plan
bun run convert --to pi
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises'
async function skillDirIsSymlink(root: string, skillName: string): Promise<boolean> {
  try { return (await lstat(path.join(root, skillName))).isSymbolicLink() } catch { return false }
}
// before install: if (await skillDirIsSymlink(skillsRoot, name)) remove or accept the skip

Type guard

function statIsSymlink(stat: { isSymbolicLink(): boolean } | null): boolean {
  return stat?.isSymbolicLink() === true
}

Prevention

When it happens

Trigger: Running a Pi bundle write/cleanup when a skill directory under the managed skills root exists and lstat reports isSymbolicLink(); reached via the `preserved` path of cleanup.

Common situations: Dotfiles setups symlinking individual skill directories (stow/chezmoi); users linking a skill dir to a shared checkout or a customized copy; previous manual replacement of an installed skill with a link.

Related errors


AI-assisted analysis of EveryInc/compound-engineering-plugin@c9c10f8c75 (2026-08-31). Data as JSON: /api/errors/e6b9486cd6f59066. Report an issue: GitHub.