Skip to content

Detect and repoint dangling LaunchAgents symlinks - #20

Open
technicalpickles wants to merge 4 commits into
mainfrom
launchagent-dangling-symlink-and-doctor
Open

Detect and repoint dangling LaunchAgents symlinks#20
technicalpickles wants to merge 4 commits into
mainfrom
launchagent-dangling-symlink-and-doctor

Conversation

@technicalpickles

Copy link
Copy Markdown
Owner

Summary

  • symlinks.sh didn't detect a pre-existing dangling ~/Library/LaunchAgents symlink whose target moved within the repo (e.g. a platform-gating move like LaunchAgents/foo.plist -> LaunchAgents/arm64-macos/foo.plist).
  • A stale symlink like this silently kept an old LaunchAgent job loaded for weeks: launchd runs the already-loaded job from memory and won't notice a moved plist until the next full reboot re-scan, and that re-scan can itself silently fail on a still-dangling symlink.
  • Closes gt-ap4x.

Changes

  • functions.sh: new repoint_dangling_launchagents() scans ~/Library/LaunchAgents/*.plist for dangling symlinks, finds the matching repo plist by basename (same top-level/arm64-macos precedence symlinks.sh already uses), repoints via the existing link() helper, and force-reloads via launchctl unload/load.
  • symlinks.sh: wires the new call into the existing running_macos block, before the LaunchAgents linking loops.
  • scripts/test-repoint-dangling-launchagents.sh: manual test harness (this repo has no automated shell-test framework; see the existing scripts/test-link.sh for 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 in functions.sh instead, next to the link() 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

Requirement Status
Detects symlinks under ~/Library/LaunchAgents/ whose basename matches a repo-tracked plist Met
Detects when such a symlink is dangling Met
Repoints it to the plist's current location Met
Reloads it via launchctl unload/load Met

4/4 met. Note: launchctl reload 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.


🤖 AI Disclosure: This PR was authored by Claude AI Agent, orchestrated by @technicalpickles. All code, tests, and this PR description were AI-generated under human supervision. Please review with this context in mind.

technicalpickles and others added 4 commits July 29, 2026 11:27
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
technicalpickles marked this pull request as ready for review July 29, 2026 19:00
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.

1 participant