fix(pi): preserve user-managed symlinks and unmanaged paths on install#1089
Conversation
Installing to pi unconditionally removed any existing directory at the target skill/agent path and replaced it with a fresh upstream copy, even when the path was a user-managed symlink pointing at a personal fork or a manually authored directory. This silently deactivated local patches that rely on the standard symlink-override workflow. The skill and agent cleanup helpers now lstat the target first: - symlinks (including dangling ones) are preserved with a warning - real paths not recorded in the install manifest are preserved with a warning - manifest-owned real paths are replaced as before Preserved entries are skipped for both cleanup and the subsequent copy/write (writing through a preserved symlink would clobber the fork), and are excluded from the freshly written install manifest so the plugin never claims ownership of a path it did not write. The removed-entry sweeps apply the same symlink guard so a stale manifest entry cannot delete through a user symlink on a later run. Fixes EveryInc#1048 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b1afd4f9d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The legacy cleanup pass runs after the install loops and fingerprints legacy-named paths by reading content, which follows symlinks — so a user fork of a legacy CE skill matched and its symlink node was renamed into legacy-backup, deactivating the override the earlier guards had just preserved. Guard moveLegacyArtifactToBackup with the same lstat check so all five legacy sweeps preserve symlink nodes. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Addressed the Codex P1 (legacy sweep followed symlinks). Root cause: Fix: guard the single destructive choke point, if (await isPreservedSymlink(artifactPath)) return
if (!(await pathExists(artifactPath))) returnGuarding the choke point rather than the two call-site loops reuses the existing Test: added a regression mirroring the exact repro — a symlinked Verified red-without-guard / green-with-guard; The |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9a4f854c0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Fixes #1048
Summary
Installing to Pi no longer destroys local skill overrides. Previously,
compound-plugin install <plugin> --to pireplaced whatever sat at each target skill/agent path with a fresh upstream copy — including user-managed symlinks pointing at a personal fork or worktree, and manually authored directories. A user following the standard symlink-override pattern (the basis of downstream patch projects likeagents-skills) had their patches silently deactivated on every install, with no warning and nothing in the output to indicate it happened.Current behavior
From #1048, after creating the standard override symlink and reinstalling:
The skill/agent cleanup helpers in
src/targets/pi.tsrunfs.rm(..., { recursive: true, force: true })and then copy, with no check for what the existing path is.Fix
The cleanup helpers now
lstatthe target before acting, implementing the contract from the issue:Three decisions worth reviewer attention:
rmand the subsequent copy/write.copySkillDirmerges without pre-clearing, so writing through a preserved symlink would push upstream content into the user's fork — worse than the original bug.cleanupRemovedSkills/cleanupRemovedAgentsapply the same symlink guard, covering the case where a skill is dropped from the bundle while a stale manifest entry still claims a path the user has since converted to a symlink.The guard covers the
skillDirs,generatedSkills, and agents loops alike.Verification
tests/pi-writer.test.ts: symlinked skill dir / agent file preserved with fork content untouched and manifest exclusion; unmanaged real dir/file preserved; manifest-owned dir and agent file still replaced on reinstall; preserved symlink survives a later run where the skill/agent is dropped from the bundle; dangling symlink preserved (pins thelstat-not-statbehavior).src/targets/pi.tsrestored to main, 7 of the new tests fail; with the fix, all 19 tests in the file pass.bun testbaseline diff against main on this machine: zero new failures (the pre-existing failure set here is environment-related and identical on both).Scope notes
src/targets/codex.ts(cleanupCurrentManagedSkillDir) andsrc/targets/managed-artifacts.ts(cleanupCurrentManagedDirectory, used by the OpenCode writer) share the unconditional rm-then-copy shape and would clobber symlinks the same way. Left untouched to keep this PR reviewable against the Pi repro in Installing to pi clobbers user-managed symlinks at skills/ and agents/ paths #1048; happy to file a follow-up or extend this fix if preferred.