Development record
abcd's own test suite leaves a dangling symlink on PATH, and `ahoy install` then refuses to complete for ever, with no supported way to clear it.
abcd's own test suite leaves a dangling symlink on PATH, and ahoy install then refuses to complete for ever, with no supported way to clear it.
Found at the very start of adopting abcd in a managed repository. abcd was not on PATH at all, and the user-level bin entry was a symlink pointing into a deleted temp directory whose name is a Go test name from abcd's own ahoy install suite (a TestAhoyInstallAccepts… directory, with the numbered subdirectory a t.TempDir tree leaves behind). So a test run wrote a real symlink into the user's real bin directory and did not clean it up.
The consequence is worse than the litter. That directory is early on PATH, so every bare abcd … resolved to a broken link, including the command abcd's own SessionStart hook tells the operator to run. Detection reported it correctly as symlink.foreign and refused: "Resolve manually; ahoy refuses to clobber." That refusal is right in general — abcd must not delete a binary someone else installed — but the state is unreachable from inside the tool: there is no --force, no ahoy uninstall path that removes a foreign entry, and the gap is resolvable: false, so ahoy install can never finish. The only way out is a manual removal the operator has to be told to run.
Two things worth separating. Refusing to clobber a foreign symlink is correct. Refusing to clobber a symlink that is DANGLING is not obviously correct: a link whose target does not exist runs nothing, shadows the real binary, and cannot be anyone's working install. abcd already reads the target to report it, so it already knows the target is missing.
Needed: (1) the test suite must not write into the user's real bin directory — point it at a temp bin dir; (2) treat a dangling symlink as replaceable, or offer an explicit override that names what it is replacing, so a repo can be adopted without shell surgery. Both are small; the first is the one that created the problem.
Grounds
- pursued: we expect danglingness to be the right discriminator because a link resolving to nothing cannot be in use by anyone, so clearing it takes nothing from a user while ending a permanent wall; it is shown wrong if a dangling link is ever a deliberate placeholder someone intends to fill
Frontmatter
| id | iss-2609100506256636 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-2609100506256636-ahoy-refuses-forever-on-a-symlink-its.md
open on GitHub ↗ · commit history ↗
mentions3
- dcn-26092700000000032026-09-27 — iss-2609100506263330 takes the record's option (b): with no verified release artefact in the persistent plugin data directory, `ahoy install` writes no PATH entry and names the install one-liner as the command to run first, rather than writing a symlink into the plugin root that the next plugin update strands (lane implementer drainH, autonomous run A, orchestrator abcd-51). Option (a), fetching the artefact inside install, is not taken: install's documented meaning is local configuration, and adr-38 lets the network answer only a verb whose documented meaning is the fetch; the one fetch-and-verify primitive (`abcd update`'s) also imports ahoy, so reaching it from install would need a second downloader or an import inversion. A symlink into the plugin root that an earlier release wrote is left where it stands on a cold cache, raised as a required `symlink.legacy` gap whatever the cache holds, and recorded so the hooks accept it meanwhile. A dangling link `~/.abcd/path-entry` names (read through the same owned, not-group-or-other-writable guard the hook shims apply) is classified abcd's own and repaired by install or removed by uninstall with its record; an unrecorded dangling link keeps iss-2609100506256636's ruling — no provenance claimed, left untouched by detection and by any run with nothing to write in its place, cleared only when install writes the verified copy there.
- dcn-26092800000000052026-09-28 — iss-2609280932480608 is fixed rather than deferred (ruling by orchestrator abcd-9f, autonomous run A; lane fix2-drainH): iss-2609100506256636's rule, danglingness not provenance, applies past the one entry install acts on, so a gap-driven install removes every abcd-owned dangling `PATH` entry other than its target, with its record, and names each in a note. The removal waits for the target to be a working entry of abcd's own after the step, not merely for the run to have something to write: a cold-cache run that adopts the one-liner's copy writes nothing and still leaves `abcd` answering, while a run that leaves nothing working at the target keeps the dangling entry and names the command to run first. An unowned dangling link is never removed by this step. A dangling link runs nothing — the shell skips it — so the shadow note and the dangling gap no longer say it is what runs or that it shadows later entries. Correction to the 2026-09-27 entry above on iss-2609100506263330: an unrecorded dangling link is not cleared only when install writes the verified copy there, as that entry says, but whenever install writes an entry of its own there — the verified copy, or the `--dev` shim when the plugin binary it rebuilds beside exists; `clearDanglingEntry` clears it ahead of either write, and a run with nothing to write in its place still leaves it untouched.
- iss-2609100509531349The record verbs are sound; the failures were all at the edges, where the tool knows something and does not say it.