Detect and repoint dangling LaunchAgents symlinks - #20
Open
technicalpickles wants to merge 4 commits into
Open
Conversation
When a repo plist moves (e.g. a platform-gating move into arm64-macos/), the existing ~/Library/LaunchAgents symlink is left dangling and launchd keeps running the stale in-memory job until the next reboot's re-scan -- which can itself silently fail on a still-dangling symlink. Proactively scan for dangling LaunchAgents symlinks, repoint them to the current repo plist location using the same two-tier lookup as the existing linking loops, and force launchctl unload/load so the fix takes effect immediately.
Moves repoint_dangling_launchagents() from symlinks.sh into functions.sh (alongside the link() helper it composes with) so it can be sourced and exercised in isolation, then adds scripts/test-repoint-dangling-launchagents.sh following the scripts/test-link.sh manual-harness pattern: a temp $HOME/Library/LaunchAgents and a temp fake $DIR/LaunchAgents, launchctl stubbed out, asserting a dangling symlink gets repointed (top-level and arm64-macos-fallback cases) and that an unmatched dangling symlink is left alone with a warning rather than touched. Also drops the "per bean gt-ap4x" citation from the code comment -- this repo cites ADRs in code, not beans, since beans are archived once done. The commit that introduced the function already carries the bean ID (gt-ap4x) in its message, which is the right place for it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
symlinks.sh links top-level LaunchAgents first, then arm64-macos/ plists on top -- letting arm64-macos win on a basename collision. repoint_dangling_launchagents() checked top-level first instead, which would have picked the wrong plist for a moved/duplicated agent. Flip the lookup order to match, add a test case that pins the precedence, and document the repoint/reload behavior in LaunchAgents/README.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…launchagents Bare `local name current_repo_path` re-declares without assigning, so bash preserves current_repo_path's value across iterations of the for loop. When a later dangling symlink has no matching repo plist, it silently inherited the previous iteration's resolved path and got repointed to the wrong target instead of being correctly reported as unmatched. Explicitly assign `local current_repo_path=""` to reset it every iteration. Adds a regression test (Test 5) with two dangling symlinks in one call -- one matched, one unmatched, in the exact order that exposed the bug -- to close the coverage gap where all prior tests only ever had a single dangling plist per invocation.
technicalpickles
marked this pull request as ready for review
July 29, 2026 19:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
symlinks.shdidn't detect a pre-existing dangling~/Library/LaunchAgentssymlink whose target moved within the repo (e.g. a platform-gating move likeLaunchAgents/foo.plist->LaunchAgents/arm64-macos/foo.plist).Changes
functions.sh: newrepoint_dangling_launchagents()scans~/Library/LaunchAgents/*.plistfor dangling symlinks, finds the matching repo plist by basename (same top-level/arm64-macos precedencesymlinks.shalready uses), repoints via the existinglink()helper, and force-reloads vialaunchctl unload/load.symlinks.sh: wires the new call into the existingrunning_macosblock, before the LaunchAgents linking loops.scripts/test-repoint-dangling-launchagents.sh: manual test harness (this repo has no automated shell-test framework; see the existingscripts/test-link.shfor the same pattern) covering top-level repoint, arm64-macos fallback, orphaned-dangling-left-alone, arm64-macos-wins-on-collision precedence, and a multi-symlink regression test for a loop-variable bug found during review.LaunchAgents/README.md: documents the repoint/reload behavior.Note on scope
The original task instruction was to only modify
symlinks.sh. The detection logic ended up infunctions.shinstead, next to thelink()helper it composes with, so it could be unit-tested via this repo's existing test-harness pattern rather than shipped untested. Flagged mid-pipeline and approved as a deviation from the literal instruction.Review history
Three review rounds: round 1 found an arm64/top-level lookup-precedence warning (fixed), round 2 found a critical stale-loop-variable bug causing wrong-plist repoints when multiple dangling symlinks exist in one run (fixed, with a regression test), round 3 approved clean.
Requirements coverage
~/Library/LaunchAgents/whose basename matches a repo-tracked plistlaunchctl unload/load4/4 met. Note:
launchctlreload calls are verified via a stub in the test harness, not against real launchd. One manual smoke test on a real dangling LaunchAgent symlink is recommended before merge, per the plan's test scenario #2.