Publish skills through the agent-neutral skill home

This commit is contained in:
Gabriel Brown
2026-08-27 16:39:16 -04:00
parent cb305f6662
commit c71d6c8799
9 changed files with 104 additions and 73 deletions
+41 -20
View File
@@ -2,7 +2,7 @@
# The manual this machine hands an agent.
#
# skills/ and .claude/skills/panama exist because an agent asked to do anything
# skills/ and .agents/skills/panama exist because an agent asked to do anything
# on a Panama desktop will otherwise infer it from the source and get half of it
# wrong. That only helps if what the skills say is true -- and a skill is worse
# than no skill when it is stale, because an agent believes it verbatim and does
@@ -16,7 +16,7 @@
# That is a convention on the prose -- name things exactly, in backticks --
# and it is how they should be written anyway.
# 3. The delivery works: link-skills is a stage, in the right place, and the
# personal manifest hands ~/.claude/skills over to the linkdir kind.
# personal manifest hands both shared skill homes to the linkdir kind.
#
# Sections 1 and 2 report clearly and keep going when a skill is not written
# yet, so this contract is useful while the skills are still being authored.
@@ -32,9 +32,21 @@ manifest="$repo_dir/user/manifest"
findings=()
note() { findings+=("$1"); }
# The three, and where each is delivered from. The two under skills/ are shipped
# to every machine; the third is project-level and needs no delivery at all.
SKILL_DIRS=(skills/panama-desktop skills/panama-sudo .claude/skills/panama)
# Every directory under skills/ ships to every machine. The project-level
# panama skill stays in this repository and needs no home-directory delivery.
SKILL_DIRS=()
for directory in "$repo_dir"/skills/*; do
[[ -d "$directory" ]] && SKILL_DIRS+=("${directory#"$repo_dir"/}")
done
SKILL_DIRS+=(.agents/skills/panama)
# The project skill has one agent-neutral source. Claude gets a compatibility
# symlink, while Codex and other Agent Skills readers use .agents directly.
[[ -L "$repo_dir/.claude/skills/panama" ]] \
|| note '.claude/skills/panama is not a compatibility symlink to the agent-neutral source'
[[ "$(readlink -f "$repo_dir/.claude/skills/panama" 2>/dev/null)" == \
"$(readlink -f "$repo_dir/.agents/skills/panama" 2>/dev/null)" ]] \
|| note '.claude and .agents resolve the project panama skill differently'
# ── 1. Each skill loads ─────────────────────────────────────────────────────
@@ -54,7 +66,7 @@ for relative in "${SKILL_DIRS[@]}"; do
present+=("$directory")
# Frontmatter is the first --- delimited block, and a skill without one is
# not a skill: Claude Code skips the directory entirely.
# not a skill: agent loaders skip the directory entirely.
frontmatter="$(awk 'NR==1 { if ($0 != "---") exit 1; next } $0 == "---" { exit } { print }' "$file")"
if [[ -z "$frontmatter" ]]; then
note "$relative/SKILL.md does not open with a --- frontmatter block"
@@ -207,12 +219,12 @@ else
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
# A checkout and a home of its own. Never the real ones: ~/.claude/skills on
# this machine is somebody's live agent setup, and a contract that broke it
# mid-session would be worse than the bug it was looking for.
# A checkout and a home of its own. Never the real ones: these are somebody's
# live agent setup, and a contract that broke them mid-session would be worse
# than the bug it was looking for.
checkout="$work/Panama"
home="$work/home"
mkdir -p "$checkout/setup/scripts" "$checkout/skills/shipped" "$home/.claude"
mkdir -p "$checkout/setup/scripts" "$checkout/skills/shipped" "$home/.claude" "$home/.agents"
cp "$linker" "$checkout/setup/scripts/link-skills"
printf 'a shipped skill\n' >"$checkout/skills/shipped/SKILL.md"
@@ -220,6 +232,7 @@ else
# symlink, which is what link-user used to leave here.
mkdir -p "$work/personal"
ln -s "$work/personal" "$home/.claude/skills"
ln -s "$work/personal" "$home/.agents/skills"
run() { HOME="$home" PANAMA_PATH="$checkout" "$checkout/setup/scripts/link-skills" >"$work/log" 2>&1; }
@@ -231,22 +244,30 @@ else
|| note 'link-skills left ~/.claude/skills a symlink, so nothing else can be linked into it'
[[ -L "$home/.claude/skills/shipped" ]] \
|| note 'link-skills did not link each shipped skill as a child of ~/.claude/skills'
[[ -d "$home/.agents/skills" && ! -L "$home/.agents/skills" ]] \
|| note 'link-skills left ~/.agents/skills a symlink, so shipped and personal skills cannot coexist'
[[ -L "$home/.agents/skills/shipped" ]] \
|| note 'link-skills did not link each shipped skill as a child of ~/.agents/skills'
grep -q 'Agent skills: 1 linked' "$work/log" \
|| note 'link-skills does not report how many skills it linked'
# A real directory at a shipped skill's name is somebody's work: it moves to
# config/old rather than being deleted, the same promise the other stages
# make. A symlink is not, and must not accumulate there.
rm "$home/.claude/skills/shipped"
mkdir -p "$home/.claude/skills/shipped"
printf 'installed by hand\n' >"$home/.claude/skills/shipped/SKILL.md"
mkdir -p "$home/.claude/skills/untouched"
for skill_home in "$home/.claude/skills" "$home/.agents/skills"; do
rm -f "$skill_home/shipped"
mkdir -p "$skill_home/shipped"
printf 'installed by hand\n' >"$skill_home/shipped/SKILL.md"
mkdir -p "$skill_home/untouched"
done
run
grep -rq 'installed by hand' "$checkout/config/old" 2>/dev/null \
|| note 'link-skills destroyed a real skill instead of moving it to config/old'
[[ -d "$home/.claude/skills/untouched" ]] \
|| note 'link-skills removed a skill it does not ship'
[[ "$(grep -rl 'installed by hand' "$checkout/config/old" 2>/dev/null | wc -l)" == 2 ]] \
|| note 'link-skills did not preserve real skills from both agent homes'
for skill_home in "$home/.claude/skills" "$home/.agents/skills"; do
[[ -d "$skill_home/untouched" ]] \
|| note "link-skills removed an unshipped skill from $skill_home"
done
before="$(find "$checkout/config/old" | wc -l)"
run
@@ -287,8 +308,8 @@ fi
grep -qE '^\s*linkdir\s+agents/skills\s+~/\.claude/skills\s*$' "$manifest" \
|| note 'the manifest does not use linkdir for ~/.claude/skills, so personal skills would replace the directory'
grep -qE '^\s*link\s+agents/skills\s+~/\.agents/skills\s*$' "$manifest" \
|| note '~/.agents/skills is no longer a whole-directory link, and nothing else claims that path'
grep -qE '^\s*linkdir\s+agents/skills\s+~/\.agents/skills\s*$' "$manifest" \
|| note 'the manifest does not use linkdir for ~/.agents/skills, so personal skills would replace shipped skills'
grep -q 'linkdir)' "$user_linker" \
|| note 'link-user does not implement the linkdir kind the manifest asks for'
grep -q 'linkdir' "$repo_dir/user/README.md" \
+12 -7
View File
@@ -73,7 +73,7 @@ cat >"$checkout/user/manifest" <<'FIXTURE'
# a comment, and a blank line follow
link agents/AGENTS.md ~/.claude/CLAUDE.md
link agents/skills ~/.agents/skills
linkdir agents/skills ~/.agents/skills
linkdir agents/skills ~/.claude/skills
copy plain.txt ~/.config/plain.txt
link missing.txt ~/.config/missing.txt
@@ -102,11 +102,12 @@ PANAMA_USER_CONTENT=no run
# ── 2. Saying yes links, and keeps what was there ───────────────────────────
# ~/.claude/skills is shared now: link-skills has already made it a real
# directory and linked Panama's own skills into it. A linkdir entry has to land
# beside those rather than replace the directory holding them.
mkdir -p "$home/.claude/skills"
# Both skill homes are shared now: link-skills has already made them real
# directories and linked Panama's own skills into each. Personal linkdir
# entries land beside those rather than replacing either directory.
mkdir -p "$home/.claude/skills" "$home/.agents/skills"
ln -s "$checkout/skills/shipped" "$home/.claude/skills/shipped"
ln -s "$checkout/skills/shipped" "$home/.agents/skills/shipped"
PANAMA_USER_CONTENT=yes run
@@ -114,8 +115,12 @@ PANAMA_USER_CONTENT=yes run
|| note 'CLAUDE.md was not replaced with a symlink into the checkout'
[[ "$(cat "$home/.claude/CLAUDE.md")" == "tracked instructions" ]] \
|| note 'the CLAUDE.md link does not resolve to the tracked file'
[[ -L "$home/.agents/skills" && -f "$home/.agents/skills/example/SKILL.md" ]] \
|| note 'the skills directory was not linked as a directory'
[[ -d "$home/.agents/skills" && ! -L "$home/.agents/skills" ]] \
|| note 'a linkdir entry replaced ~/.agents/skills with a symlink'
[[ -L "$home/.agents/skills/example" && -f "$home/.agents/skills/example/SKILL.md" ]] \
|| note 'a linkdir entry did not publish the personal skill for agent-neutral readers'
[[ -L "$home/.agents/skills/shipped" ]] \
|| note 'a linkdir entry removed the shipped skill from ~/.agents/skills'
[[ -d "$home/.claude/skills" && ! -L "$home/.claude/skills" ]] \
|| note 'a linkdir entry replaced its destination directory with a symlink'
[[ -L "$home/.claude/skills/example" && -f "$home/.claude/skills/example/SKILL.md" ]] \