Skip to content

Fix CLI install recovery from stale App Translocation symlinks#413

Merged
baron merged 1 commit into
repoprompt:mainfrom
amittell:fix/cli-stale-translocation-symlink
Jul 8, 2026
Merged

Fix CLI install recovery from stale App Translocation symlinks#413
baron merged 1 commit into
repoprompt:mainfrom
amittell:fix/cli-stale-translocation-symlink

Conversation

@amittell

@amittell amittell commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Repair CE user-space CLI symlinks that were created while the app ran under macOS App Translocation and later became dangling once the app moved to a stable location.
  • Preserve the existing "refuse to touch unmanaged files" safety boundary by only reclaiming translocated targets whose app-bundle-relative suffix matches a known managed destination.
  • Make InstallError.pathExistsNotOurs carry the actual offending path instead of always reporting /usr/local/bin/rpce-cli.

Symptom

Installing the rpce-cli CLI (Settings → MCP → CLI Tools → Install) fails with:

Install failed: A file already exists at /usr/local/bin/rpce-cli that wasn't created by RepoPrompt

even though /usr/local/bin/rpce-cli does not exist. This reproduces for anyone who first launched the app from a quarantined location (Downloads/DMG, i.e. App Translocation) and later moved it to /Applications.

Root cause

The install failure originates at the user-space symlink step, not the /usr/local/bin shim:

  1. CLIPathInstaller.install() finds the PATH shim notInstalled, then calls CLISymlinkManagerUserSpace.ensureLocalSymlink(...).
  2. ensureLocalSymlink classifies the existing user-space symlink via ManagedCLIPathPolicy.classifySymlink(...).
  3. That symlink was created during a translocated launch, so it points at a randomized path such as /private/var/folders/.../AppTranslocation/<uuid>/d/RepoPrompt CE.app/Contents/MacOS/repoprompt-mcp.
  4. After the app moves to /Applications, that target disappears and is not on the managed-destination allowlist, so classifySymlink returns .unmanaged.
  5. ensureLocalSymlink refuses to replace .unmanaged entries and returns false; install() throws .pathExistsNotOurs.

Second, independent bug: InstallError.pathExistsNotOurs had no associated value and always formatted its message with installPath, so a user-space symlink (or claude-rpce wrapper) conflict was reported as /usr/local/bin/rpce-cli, a path that may not exist.

Fix

ManagedCLIPathPolicy.classifySymlink(...) now also treats a destination as managed-stale (repairable) when:

  • the destination path contains /AppTranslocation/, and
  • its suffix from the last *.app component to the end matches the same suffix of a known managed destination (e.g. RepoPrompt CE.app/Contents/MacOS/repoprompt-mcp).

This keeps unrelated paths unmanaged (an unrelated translocated *.app, or plain foreign symlinks stay .unmanaged), and leaves current/stale/normal classifications unchanged.

InstallError.pathExistsNotOurs now carries path: String; each throw site passes the path that actually conflicted (the PATH shim, the user-space symlink, or the claude-rpce wrapper).

Proof

Standalone harness that mirrors classifySymlink and toggles only the new translocation branch, run against real on-disk symlinks:

=== BEFORE FIX ===
  valid current link -> bundled CLI                 -> managedCurrent
  stale translocation link (OUR app, vanished)      -> unmanaged     (REFUSED -> install fails)
  foreign translocation link (other .app)           -> unmanaged
  plain foreign link                                -> unmanaged

=== AFTER FIX ===
  valid current link -> bundled CLI                 -> managedCurrent
  stale translocation link (OUR app, vanished)      -> managedStale  (reclaimable -> repaired)
  foreign translocation link (other .app)           -> unmanaged
  plain foreign link                                -> unmanaged

Local reproduction confirmed the user-space symlink pointed at a deleted AppTranslocation target while /usr/local/bin/rpce-cli did not exist, matching both the misleading message and the refusal to repair.

Tests

Added to CLIPathInstallerTests:

  • testClassifierReclaimsStaleTranslocatedManagedTarget — stale translocated managed target is .managedStale; a non-matching translocated *.app suffix stays .unmanaged.
  • testUserSpaceManagerRepairsStaleTranslocatedSymlinkensureLocalSymlink repairs the stale translocated link (direct regression for the reported bug).
  • testPathExistsNotOursErrorReportsProvidedPath — the error message names the provided path.

Validation notes

  • xcrun swiftc -parse on both edited source files: no syntax errors.
  • Standalone classifier harness: expected before/after behavior (above).
  • make dev-test FILTER=CLIPathInstallerTests, make dev-lint, and make dev-swift-build could not run in the contributor environment because it is Command Line Tools–only and hits the existing PreviewsMacros plugin limitation (issue CLT-only builds fail despite doctor.sh passing — Xcode-only macro plugins #174); the XCTest target cannot compile there. These lanes should run in CI / on a full-Xcode machine, and the added tests are written to run there.

Relationship to #278

Complementary and non-conflicting. #278 relocates the user-space symlink path in MCPFilesystemIdentity.swift (a hard cutover) and would incidentally sidestep the symptom by targeting a fresh path; it does not fix the classifier logic or the misleading error text. This PR touches only ManagedCLIPathPolicy.swift and CLIPathInstaller.swift.

Conversation: https://app.warp.dev/conversation/550683e7-eeb5-469a-894f-eb6f7360e3fc

Co-Authored-By: Oz oz-agent@warp.dev

Copilot AI review requested due to automatic review settings July 7, 2026 20:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens RepoPrompt CE’s CLI installation flow against stale user-space symlinks created while the app was running under macOS App Translocation, and improves install error reporting so conflicts name the actual offending path.

Changes:

  • Extend ManagedCLIPathPolicy.classifySymlink to treat certain translocated (AppTranslocation) symlink targets as “managed stale” when their app-bundle-relative suffix matches an allowlisted managed destination.
  • Update CLIPathInstaller.InstallError.pathExistsNotOurs to carry the conflicting path and update throw sites accordingly.
  • Add focused XCTest coverage for the new translocation reclaim behavior, user-space symlink repair, and the improved error message.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
Tests/RepoPromptTests/MCP/CLIPathInstallerTests.swift Adds regression tests for translocated-stale symlink reclaim/repair and for correct path reporting in pathExistsNotOurs.
Sources/RepoPrompt/Infrastructure/Process/CLI/ManagedCLIPathPolicy.swift Adds App Translocation-aware managed-stale classification via suffix matching against known managed destinations.
Sources/RepoPrompt/Infrastructure/Process/CLI/CLIPathInstaller.swift Threads the actual conflicting path through pathExistsNotOurs and updates throw sites to report accurate locations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sources/RepoPrompt/Infrastructure/Process/CLI/ManagedCLIPathPolicy.swift Outdated
Comment thread Sources/RepoPrompt/Infrastructure/Process/CLI/CLIPathInstaller.swift Outdated
amittell added a commit to amittell/repoprompt-ce that referenced this pull request Jul 7, 2026
install() previously threw pathExistsNotOurs whenever ensureLocalSymlink returned false, so non-conflict failures (support-directory creation, atomic swap/validation) wrongly claimed a conflicting file exists. Addresses Copilot review feedback on PR repoprompt#413.

Add InstallError.userSymlinkSetupFailed(path:) and a testable userSymlinkInstallError(userLink:bundledPath:) mapping that reports pathExistsNotOurs only when the user-space path is a genuine unmanaged occupant, and a setup-failure message otherwise.

Co-Authored-By: Oz <oz-agent@warp.dev>
@amittell amittell force-pushed the fix/cli-stale-translocation-symlink branch from 6c4f563 to d424274 Compare July 8, 2026 02:32
@amittell

amittell commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Please review the updated PR. Both original Copilot comments have been addressed in the squashed commit: (1) Substring check replaced with path-component match. (2) Added InstallError.userSymlinkSetupFailed and userSymlinkInstallError to distinguish genuine conflicts from other setup failures, with regression tests.

The CE user-space CLI symlink created while the app runs under macOS App Translocation points at a randomized /AppTranslocation/<uuid>/... path. After the app moves to a stable location that target disappears and is absent from the managed-destination allowlist, so classifySymlink returned .unmanaged and ensureLocalSymlink refused to repair it.

Changes:

- Recognize a translocated destination as managed-stale (repairable) when its app-bundle-relative suffix matches a known managed destination, while leaving unrelated .app targets and plain foreign links unmanaged.

- Make InstallError.pathExistsNotOurs carry the actual offending path so failures no longer always report /usr/local/bin/rpce-cli.

- Add InstallError.userSymlinkSetupFailed so install() no longer conflates non-conflict setup failures (directory creation, atomic swap) with file conflicts.

- Hardened AppTranslocation detection from substring to path-component match (Copilot review feedback).

Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Co-Authored-By: Oz <oz-agent@warp.dev>
@baron baron force-pushed the fix/cli-stale-translocation-symlink branch from d424274 to dea1c81 Compare July 8, 2026 21:36

@baron baron left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after maintainer review. Locally rebased onto current main (dea1c81 on 7360ea8), reviewed the CLI symlink/App Translocation fix, ran focused validation (CLIPathInstallerTests: 14 tests, 0 failures), dev-lint, and RepoPrompt product build. Claude Fable 5 High second opinion found no must-fix issues.

@baron baron merged commit c255548 into repoprompt:main Jul 8, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants