feat(shell): deliver project skills through .agents
This commit is contained in:
@@ -53,68 +53,75 @@ pkgs.runCommandLocal "skills-shell-hook-check"
|
|||||||
# The fixture project carries three things the hook must not disturb: a
|
# The fixture project carries three things the hook must not disturb: a
|
||||||
# hand-authored skill under its own name, a hand-authored skill whose name
|
# hand-authored skill under its own name, a hand-authored skill whose name
|
||||||
# collides with a selected one, and a symlink pointing outside the store.
|
# collides with a selected one, and a symlink pointing outside the store.
|
||||||
mkdir -p proj/.claude/skills/handmade
|
mkdir -p proj/.agents/skills/handmade
|
||||||
printf '%s\n' "a hand-authored skill" > proj/.claude/skills/handmade/SKILL.md
|
printf '%s\n' "a hand-authored skill" > proj/.agents/skills/handmade/SKILL.md
|
||||||
mkdir -p proj/.claude/skills/sample-gamma
|
mkdir -p proj/.agents/skills/sample-gamma
|
||||||
printf '%s\n' "a hand-authored gamma" > proj/.claude/skills/sample-gamma/SKILL.md
|
printf '%s\n' "a hand-authored gamma" > proj/.agents/skills/sample-gamma/SKILL.md
|
||||||
ln -s /nonexistent/external-target proj/.claude/skills/external
|
ln -s /nonexistent/external-target proj/.agents/skills/external
|
||||||
cd proj
|
cd proj
|
||||||
|
|
||||||
echo "first entry: the two free skills link as store symlinks that resolve"
|
echo "first entry: the two free skills link as store symlinks that resolve"
|
||||||
printf '%s\n' "$hookBoth" > ../hook-both.sh
|
printf '%s\n' "$hookBoth" > ../hook-both.sh
|
||||||
. ../hook-both.sh
|
. ../hook-both.sh
|
||||||
for name in sample-alpha sample-beta; do
|
for name in sample-alpha sample-beta; do
|
||||||
[ -L ".claude/skills/$name" ] || fail "$name was not linked as a symlink"
|
[ -L ".agents/skills/$name" ] || fail "$name was not linked as a symlink"
|
||||||
case "$(readlink ".claude/skills/$name")" in
|
case "$(readlink ".agents/skills/$name")" in
|
||||||
${builtins.storeDir}/*) : ;;
|
${builtins.storeDir}/*) : ;;
|
||||||
*) fail "$name's symlink does not point into the store" ;;
|
*) fail "$name's symlink does not point into the store" ;;
|
||||||
esac
|
esac
|
||||||
test -f ".claude/skills/$name/SKILL.md" \
|
test -f ".agents/skills/$name/SKILL.md" \
|
||||||
|| fail "$name's link does not resolve to its SKILL.md"
|
|| fail "$name's link does not resolve to its SKILL.md"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "a selected skill colliding with a hand-authored one leaves the real directory"
|
echo "a selected skill colliding with a hand-authored one leaves the real directory"
|
||||||
{ [ -d .claude/skills/sample-gamma ] && [ ! -L .claude/skills/sample-gamma ]; } \
|
{ [ -d .agents/skills/sample-gamma ] && [ ! -L .agents/skills/sample-gamma ]; } \
|
||||||
|| fail "the colliding hand-authored skill was replaced by a symlink"
|
|| fail "the colliding hand-authored skill was replaced by a symlink"
|
||||||
grep -qx "a hand-authored gamma" .claude/skills/sample-gamma/SKILL.md \
|
grep -qx "a hand-authored gamma" .agents/skills/sample-gamma/SKILL.md \
|
||||||
|| fail "the colliding hand-authored skill's content was overwritten"
|
|| fail "the colliding hand-authored skill's content was overwritten"
|
||||||
|
|
||||||
echo "the non-colliding hand-authored skill is an untouched real directory"
|
echo "the non-colliding hand-authored skill is an untouched real directory"
|
||||||
{ [ -d .claude/skills/handmade ] && [ ! -L .claude/skills/handmade ]; } \
|
{ [ -d .agents/skills/handmade ] && [ ! -L .agents/skills/handmade ]; } \
|
||||||
|| fail "the hand-authored skill was replaced or removed"
|
|| fail "the hand-authored skill was replaced or removed"
|
||||||
test -f .claude/skills/handmade/SKILL.md || fail "the hand-authored skill lost its content"
|
test -f .agents/skills/handmade/SKILL.md || fail "the hand-authored skill lost its content"
|
||||||
|
|
||||||
echo "a symlink pointing outside the store is not ours, so it is left alone"
|
echo "a symlink pointing outside the store is not ours, so it is left alone"
|
||||||
[ -L .claude/skills/external ] || fail "the non-store symlink was removed"
|
[ -L .agents/skills/external ] || fail "the non-store symlink was removed"
|
||||||
|
|
||||||
|
echo "Claude Code compatibility points at the canonical skill directory"
|
||||||
|
[ -L .claude/skills ] || fail ".claude/skills is not a compatibility symlink"
|
||||||
|
[ "$(readlink .claude/skills)" = "../.agents/skills" ] \
|
||||||
|
|| fail ".claude/skills points somewhere other than ../.agents/skills"
|
||||||
|
test -f .claude/skills/sample-alpha/SKILL.md \
|
||||||
|
|| fail "Claude Code compatibility link does not resolve selected skills"
|
||||||
|
|
||||||
echo "the .gitignore lists only the skills the hook actually linked, plus itself"
|
echo "the .gitignore lists only the skills the hook actually linked, plus itself"
|
||||||
grep -qx '/.gitignore' .claude/skills/.gitignore || fail ".gitignore does not ignore itself"
|
grep -qx '/.gitignore' .agents/skills/.gitignore || fail ".gitignore does not ignore itself"
|
||||||
grep -qx '/sample-alpha' .claude/skills/.gitignore || fail ".gitignore omits sample-alpha"
|
grep -qx '/sample-alpha' .agents/skills/.gitignore || fail ".gitignore omits sample-alpha"
|
||||||
grep -qx '/sample-beta' .claude/skills/.gitignore || fail ".gitignore omits sample-beta"
|
grep -qx '/sample-beta' .agents/skills/.gitignore || fail ".gitignore omits sample-beta"
|
||||||
for tracked in /handmade /sample-gamma /external; do
|
for tracked in /handmade /sample-gamma /external; do
|
||||||
grep -qx "$tracked" .claude/skills/.gitignore \
|
grep -qx "$tracked" .agents/skills/.gitignore \
|
||||||
&& fail ".gitignore lists $tracked, which would untrack a real path"
|
&& fail ".gitignore lists $tracked, which would untrack a real path"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "no manifest or state file is written: only the fixtures and links exist"
|
echo "no manifest or state file is written: only the fixtures and links exist"
|
||||||
entries=$(ls -A .claude/skills | sort | tr '\n' ' ')
|
entries=$(ls -A .agents/skills | sort | tr '\n' ' ')
|
||||||
[ "$entries" = ".gitignore external handmade sample-alpha sample-beta sample-gamma " ] \
|
[ "$entries" = ".gitignore external handmade sample-alpha sample-beta sample-gamma " ] \
|
||||||
|| fail "unexpected entries under .claude/skills: [$entries]"
|
|| fail "unexpected entries under .agents/skills: [$entries]"
|
||||||
|
|
||||||
echo "second entry with sample-beta deselected: its symlink is removed, the rest stay"
|
echo "second entry with sample-beta deselected: its symlink is removed, the rest stay"
|
||||||
printf '%s\n' "$hookAlpha" > ../hook-alpha.sh
|
printf '%s\n' "$hookAlpha" > ../hook-alpha.sh
|
||||||
. ../hook-alpha.sh
|
. ../hook-alpha.sh
|
||||||
test ! -e .claude/skills/sample-beta || fail "the deselected skill's symlink was not removed"
|
test ! -e .agents/skills/sample-beta || fail "the deselected skill's symlink was not removed"
|
||||||
[ -L .claude/skills/sample-alpha ] || fail "the still-selected skill was dropped"
|
[ -L .agents/skills/sample-alpha ] || fail "the still-selected skill was dropped"
|
||||||
{ [ -d .claude/skills/handmade ] && [ ! -L .claude/skills/handmade ]; } \
|
{ [ -d .agents/skills/handmade ] && [ ! -L .agents/skills/handmade ]; } \
|
||||||
|| fail "the hand-authored skill was disturbed on re-entry"
|
|| fail "the hand-authored skill was disturbed on re-entry"
|
||||||
{ [ -d .claude/skills/sample-gamma ] && [ ! -L .claude/skills/sample-gamma ]; } \
|
{ [ -d .agents/skills/sample-gamma ] && [ ! -L .agents/skills/sample-gamma ]; } \
|
||||||
|| fail "the colliding hand-authored skill was disturbed on re-entry"
|
|| fail "the colliding hand-authored skill was disturbed on re-entry"
|
||||||
[ -L .claude/skills/external ] || fail "the non-store symlink was disturbed on re-entry"
|
[ -L .agents/skills/external ] || fail "the non-store symlink was disturbed on re-entry"
|
||||||
|
|
||||||
echo "the .gitignore tracks the new selection"
|
echo "the .gitignore tracks the new selection"
|
||||||
grep -qx '/sample-alpha' .claude/skills/.gitignore || fail ".gitignore lost sample-alpha"
|
grep -qx '/sample-alpha' .agents/skills/.gitignore || fail ".gitignore lost sample-alpha"
|
||||||
grep -qx '/sample-beta' .claude/skills/.gitignore \
|
grep -qx '/sample-beta' .agents/skills/.gitignore \
|
||||||
&& fail ".gitignore still lists the deselected sample-beta"
|
&& fail ".gitignore still lists the deselected sample-beta"
|
||||||
|
|
||||||
touch "$out"
|
touch "$out"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
{
|
{
|
||||||
lib = { inherit mkSkill mkSkillsShellHook; };
|
lib = { inherit mkSkill mkSkillsShellHook; };
|
||||||
|
|
||||||
# Global placement: an operator selects skills into `~/.claude/skills/`
|
# Global placement: an operator selects skills into enabled agent harnesses
|
||||||
# via `programs.agents.skills`. Not per-system — it is a module function,
|
# via `programs.agents.skills`. Not per-system — it is a module function,
|
||||||
# and the skills it places come from the importing configuration's own
|
# and the skills it places come from the importing configuration's own
|
||||||
# selection rather than this flake's nixpkgs.
|
# selection rather than this flake's nixpkgs.
|
||||||
@@ -95,8 +95,8 @@
|
|||||||
|
|
||||||
# A working shell for developing skills in this repo.
|
# A working shell for developing skills in this repo.
|
||||||
# python3 runs the benchmark skill's deterministic core.
|
# python3 runs the benchmark skill's deterministic core.
|
||||||
# The benchmark-skill is placed into ./.claude/skills/ on entry, so
|
# The benchmark-skill is placed into ./.agents/skills/ on entry, with
|
||||||
# /benchmark-skill is available while working in this repo.
|
# ./.claude/skills as a compatibility symlink.
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = [ pkgs.python3 ];
|
packages = [ pkgs.python3 ];
|
||||||
shellHook = mkSkillsShellHook [ (skillPackages pkgs).benchmark-skill ];
|
shellHook = mkSkillsShellHook [ (skillPackages pkgs).benchmark-skill ];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Builds a dev-shell shellHook that links a project's selected skills into its
|
# Builds a dev-shell shellHook that links a project's selected skills into its
|
||||||
# own `.claude/skills/` as direct-child symlinks into the Nix store.
|
# own `.agents/skills/` as direct-child symlinks into the Nix store, with
|
||||||
|
# `.claude/skills` as a compatibility symlink when that path is free.
|
||||||
skills:
|
skills:
|
||||||
let
|
let
|
||||||
storeDir = builtins.storeDir;
|
storeDir = builtins.storeDir;
|
||||||
@@ -8,7 +9,7 @@ let
|
|||||||
);
|
);
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
# Places this project's Nix-delivered skills into ./.claude/skills/ on shell
|
# Places this project's Nix-delivered skills into ./.agents/skills/ on shell
|
||||||
# entry.
|
# entry.
|
||||||
# Each run first drops every direct-child symlink pointing into the store,
|
# Each run first drops every direct-child symlink pointing into the store,
|
||||||
# then relinks the current selection.
|
# then relinks the current selection.
|
||||||
@@ -16,12 +17,26 @@ in
|
|||||||
# skills for free and needs no manifest.
|
# skills for free and needs no manifest.
|
||||||
# A real (non-symlink) skill directory is a hand-authored, project-private
|
# A real (non-symlink) skill directory is a hand-authored, project-private
|
||||||
# skill and is left untouched.
|
# skill and is left untouched.
|
||||||
|
# When ./.claude/skills is free, it is kept as a compatibility symlink to the
|
||||||
|
# canonical ./.agents/skills directory so Claude Code sees the same skills.
|
||||||
# The whole hook runs in a subshell so it leaks no names into the interactive
|
# The whole hook runs in a subshell so it leaks no names into the interactive
|
||||||
# shell.
|
# shell.
|
||||||
(
|
(
|
||||||
_mkskills_dir=".claude/skills"
|
_mkskills_dir=".agents/skills"
|
||||||
mkdir -p "$_mkskills_dir"
|
mkdir -p "$_mkskills_dir"
|
||||||
|
|
||||||
|
mkdir -p .claude
|
||||||
|
if [ ! -e .claude/skills ] && [ ! -L .claude/skills ]; then
|
||||||
|
ln -s ../.agents/skills .claude/skills
|
||||||
|
elif [ -L .claude/skills ]; then
|
||||||
|
case "$(readlink .claude/skills)" in
|
||||||
|
../.agents/skills) : ;;
|
||||||
|
${storeDir}/*) rm -f .claude/skills && ln -s ../.agents/skills .claude/skills ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
printf 'mkSkillsShellHook: leaving existing non-symlink .claude/skills in place; Claude Code compatibility link not installed\n' >&2
|
||||||
|
fi
|
||||||
|
|
||||||
for _mkskills_entry in "$_mkskills_dir"/*; do
|
for _mkskills_entry in "$_mkskills_dir"/*; do
|
||||||
[ -L "$_mkskills_entry" ] || continue
|
[ -L "$_mkskills_entry" ] || continue
|
||||||
case "$(readlink "$_mkskills_entry")" in
|
case "$(readlink "$_mkskills_entry")" in
|
||||||
|
|||||||
Reference in New Issue
Block a user