feat(install): serve several sources instead of displacing them
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Failing after 1s

~/.agents/skills was a single link straight at this checkout. That encoded an
assumption nobody had stated — that this checkout is the only source of skills
on the host. On flamingo it was not: Crush's directory held Anthropic's
official set, and the only way to install this library was to move that set
aside. A capability loss dressed up as a tidy-up.

The canonical location becomes a real directory of per-skill symlinks, fed by
the checkout plus anything in SKILLS_EXTRA_SOURCES. Harnesses keep their one
directory link, so the count is N + one per harness rather than N x harnesses,
which is what #23 was actually about.

The three requirements #27 named, each with a test:
- collisions are logged with both paths and the checkout wins, never resolved
  in silence;
- removing a skill from a source prunes its link;
- anything in the managed directory that is not our symlink is reported and
  left in place — the installer does not delete content it did not create.

doctor learns the same model: sources are recorded at ~/.agents/skills-sources,
a link into a configured source is healthy, a link into an unknown one is not,
and the old count check becomes a per-skill check because a per-skill layout
can lose one skill without changing any total. The pre-#27 wiring is named as
legacy rather than reported as damage. count_skills needed find -L; without it
symlinked skill directories count as zero.

Verified live on koala: migrated in place to 42 per-skill links, then a second
source added a 43rd alongside them, then a re-run without that source pruned it
back to 42. Only doctor finding throughout was the genuine stale plugin.

Closes #27
Bump-Type: minor
This commit is contained in:
2026-08-29 10:05:46 +02:00
parent eee62e4fc0
commit 025f01aa80
6 changed files with 443 additions and 66 deletions
+48 -22
View File
@@ -37,11 +37,21 @@ new_fixture() {
printf '%s' "$_f"
}
# wire_clean <fixture> — the correct wiring: one canonical link, harnesses
# pointing at it.
# wire_clean <fixture> [extra-source-dir] — the correct wiring since #27: the
# canonical location is a REAL directory of per-skill links, fed by one or more
# sources, with each harness keeping its single directory link to it.
wire_clean() {
mkdir -p "$1/home/.agents" "$1/home/.config/crush" "$1/home/.config/opencode"
ln -sfn "$1/ck/skills" "$1/home/.agents/skills"
mkdir -p "$1/home/.agents/skills" "$1/home/.config/crush" "$1/home/.config/opencode"
for _d in "$1"/ck/skills/*/; do
ln -sfn "${_d%/}" "$1/home/.agents/skills/$(basename "$_d")"
done
printf '%s\n' "$1/ck/skills" > "$1/home/.agents/skills-sources"
if [ -n "${2:-}" ]; then
for _d in "$2"/*/; do
ln -sfn "${_d%/}" "$1/home/.agents/skills/$(basename "$_d")"
done
printf '%s\n' "$2" >> "$1/home/.agents/skills-sources"
fi
ln -sfn "$1/home/.agents/skills" "$1/home/.config/crush/skills"
ln -sfn "$1/home/.agents/skills" "$1/home/.config/opencode/skills"
}
@@ -79,27 +89,43 @@ assert_clean "$f" 'clean fixture exits 0'
# --- 2. dangling link (the 243-link case, gitignored and invisible) ----------
f=$(new_fixture dangling); wire_clean "$f"
ln -sfn "$f/ck/skills-that-moved" "$f/home/.agents/skills"
assert_fails "$f" 'dangling' 'dangling canonical link is reported'
ln -sfn "$f/ck/skills/vanished" "$f/home/.agents/skills/alpha"
assert_fails "$f" 'dangling' 'dangling per-skill link is reported'
# --- 3. resolves, but to a DIFFERENT checkout (the silent-stale case) --------
# --- 3. resolves, but into a source nobody configured (silent-stale case) ----
# Worse than dangling: it works, so nothing complains, while serving content
# from a checkout that is months behind.
# from a checkout that is months behind. Since #27 a link may legitimately
# point outside the checkout — but only into a source the installer recorded.
f=$(new_fixture stale)
mkdir -p "$f/other/skills/alpha" "$f/other/skills/beta"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/skills/alpha/SKILL.md"
printf -- '---\nname: beta\ndescription: b\n---\n' > "$f/other/skills/beta/SKILL.md"
mkdir -p "$f/other/alpha"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/alpha/SKILL.md"
wire_clean "$f"
ln -sfn "$f/other/skills" "$f/home/.agents/skills"
assert_fails "$f" 'outside' 'link into a foreign checkout is reported'
ln -sfn "$f/other/alpha" "$f/home/.agents/skills/alpha"
assert_fails "$f" 'unknown source' 'link into an unconfigured source is reported'
# --- 4. count mismatch (a harness seeing fewer skills than the checkout) -----
f=$(new_fixture count)
mkdir -p "$f/home/.config/crush/skills/alpha"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
mkdir -p "$f/home/.agents"
# --- 3b. a link into a CONFIGURED extra source is healthy -------------------
# The whole point of #27: Anthropic's official set alongside this library.
f=$(new_fixture multisource)
mkdir -p "$f/other/xlsx"
printf -- '---\nname: xlsx\ndescription: x\n---\n' > "$f/other/xlsx/SKILL.md"
wire_clean "$f" "$f/other"
assert_clean "$f" 'a link into a configured extra source is healthy'
# --- 3c. the pre-#27 single-link wiring is named, not mistaken for damage ----
f=$(new_fixture legacy)
mkdir -p "$f/home/.agents" "$f/home/.config/crush" "$f/home/.config/opencode"
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
assert_fails "$f" 'count' 'skill-count mismatch is reported'
ln -sfn "$f/home/.agents/skills" "$f/home/.config/crush/skills"
ln -sfn "$f/home/.agents/skills" "$f/home/.config/opencode/skills"
assert_fails "$f" 'single-link' 'legacy single-link wiring is reported as such'
# --- 4. a skill in the checkout with no link anywhere ------------------------
# The multi-source replacement for the old count check: with per-skill links a
# skill can be missing individually, which a total count would hide if an extra
# source happened to supply the same number.
f=$(new_fixture missing); wire_clean "$f"
rm -f "$f/home/.agents/skills/beta"
assert_fails "$f" 'beta' 'a checkout skill with no link is reported'
# --- 5. stray entry (the loose secret-hygiene.md that loaded nowhere) --------
f=$(new_fixture stray); wire_clean "$f"
@@ -119,9 +145,9 @@ assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported'
# resolution target, which is true but useless. It must say the dir is not a
# link at all, because the fix (move it aside, re-run install.sh) is different
# from the fix for a link into a foreign checkout.
f=$(new_fixture realdir)
mkdir -p "$f/home/.agents" "$f/home/.config/crush/skills/alpha"
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
f=$(new_fixture realdir); wire_clean "$f"
rm -f "$f/home/.config/crush/skills"
mkdir -p "$f/home/.config/crush/skills/alpha"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
assert_fails "$f" 'not a link' 'real directory is reported as unmanaged, not as "outside"'