feat(home-manager): place skills for Pi

This commit is contained in:
2026-07-29 08:07:09 -04:00
parent ea8e2c50bc
commit a4b88f8f42
2 changed files with 98 additions and 45 deletions

View File

@@ -87,15 +87,38 @@ let
programs.agents.skills = [ skillAlpha ]; programs.agents.skills = [ skillAlpha ];
}; };
# Claude Code disabled, with a skill still selected. # Pi enabled and Claude Code disabled.
claudeCodeOff = homeFiles { piOnly = homeFiles {
programs.claude-code.enable = false; programs.pi-coding-agent.enable = true;
programs.agents.skills = [ skillAlpha ]; programs.agents.skills = [ skillAlpha ];
}; };
# An empty selection with Claude Code enabled. # Both supported harnesses enabled.
bothHarnesses = homeFiles {
programs.claude-code.enable = true;
programs.pi-coding-agent.enable = true;
programs.agents.skills = [ skillAlpha ];
};
# A non-default Pi `configDir`, so the assertions can prove placement follows it
# rather than a hardcoded `.pi/agent`.
customPiConfigDir = homeFiles {
programs.pi-coding-agent.enable = true;
programs.pi-coding-agent.configDir = "/home/operator/.config/pi/agent";
programs.agents.skills = [ skillAlpha ];
};
# No compatible harness enabled, with a skill still selected.
agentsOff = homeFiles {
programs.claude-code.enable = false;
programs.pi-coding-agent.enable = false;
programs.agents.skills = [ skillAlpha ];
};
# An empty selection with both supported harnesses enabled.
emptySelection = homeFiles { emptySelection = homeFiles {
programs.claude-code.enable = true; programs.claude-code.enable = true;
programs.pi-coding-agent.enable = true;
programs.agents.skills = [ ]; programs.agents.skills = [ ];
}; };
in in
@@ -107,7 +130,10 @@ pkgs.runCommandLocal "skills-home-manager-module-check"
attrSetSkills attrSetSkills
wholeDirSkills wholeDirSkills
customConfigDir customConfigDir
claudeCodeOff piOnly
bothHarnesses
customPiConfigDir
agentsOff
emptySelection emptySelection
; ;
} }
@@ -142,13 +168,35 @@ pkgs.runCommandLocal "skills-home-manager-module-check"
test ! -e "$customConfigDir/.claude/skills/sample-alpha" \ test ! -e "$customConfigDir/.claude/skills/sample-alpha" \
|| fail "skill was placed under a hardcoded .claude, ignoring configDir" || fail "skill was placed under a hardcoded .claude, ignoring configDir"
echo "Claude Code disabled: no skill entry is written despite a selection" echo "Pi enabled: selected skills land in Pi's global skills directory"
test ! -e "$claudeCodeOff/.claude/skills/sample-alpha" \ test -f "$piOnly/.pi/agent/skills/sample-alpha/SKILL.md" \
|| fail "a skill was placed with claude-code disabled" || fail "selected skill did not land under Pi's configDir"
test ! -e "$piOnly/.claude/skills/sample-alpha" \
|| fail "a skill was placed for Claude Code when only Pi was enabled"
echo "both harnesses enabled: selected skills land in both global skills directories"
test -f "$bothHarnesses/.claude/skills/sample-alpha/SKILL.md" \
|| fail "selected skill did not land under Claude Code when both harnesses were enabled"
test -f "$bothHarnesses/.pi/agent/skills/sample-alpha/SKILL.md" \
|| fail "selected skill did not land under Pi when both harnesses were enabled"
echo "custom Pi configDir: placement follows configDir rather than a hardcoded .pi/agent"
test -f "$customPiConfigDir/.config/pi/agent/skills/sample-alpha/SKILL.md" \
|| fail "skill was not placed under Pi's configured configDir"
test ! -e "$customPiConfigDir/.pi/agent/skills/sample-alpha" \
|| fail "skill was placed under a hardcoded .pi/agent, ignoring configDir"
echo "agents disabled: no skill entry is written despite a selection"
test ! -e "$agentsOff/.claude/skills/sample-alpha" \
|| fail "a skill was placed with Claude Code disabled"
test ! -e "$agentsOff/.pi/agent/skills/sample-alpha" \
|| fail "a skill was placed with Pi disabled"
echo "empty selection: nothing is installed" echo "empty selection: nothing is installed"
test ! -e "$emptySelection/.claude/skills/sample-alpha" \ test ! -e "$emptySelection/.claude/skills/sample-alpha" \
|| fail "an unselected skill was placed" || fail "an unselected skill was placed for Claude Code"
test ! -e "$emptySelection/.pi/agent/skills/sample-alpha" \
|| fail "an unselected skill was placed for Pi"
touch "$out" touch "$out"
'' ''

View File

@@ -1,5 +1,5 @@
# Places operator-selected skills into Claude Code's skills directory, one per # Places operator-selected skills into each enabled agent's global skills
# skill, for every project. # directory, one per skill, for every project.
{ {
config, config,
lib, lib,
@@ -8,6 +8,18 @@
let let
cfg = config.programs.agents; cfg = config.programs.agents;
claudeCode = config.programs.claude-code; claudeCode = config.programs.claude-code;
pi = config.programs.pi-coding-agent;
skillFiles = configDir:
lib.listToAttrs (
map (skill: {
name = "${configDir}/skills/${skill.skillName}";
value = {
source = skill;
recursive = true;
};
}) cfg.skills
);
in in
{ {
options.programs.agents.skills = lib.mkOption { options.programs.agents.skills = lib.mkOption {
@@ -15,49 +27,42 @@ in
default = [ ]; default = [ ];
example = lib.literalExpression "[ inputs.skills.packages.\${system}.some-skill ]"; example = lib.literalExpression "[ inputs.skills.packages.\${system}.some-skill ]";
description = '' description = ''
Skills to install globally, into the `skills/` subdirectory of Skills to install globally into every enabled agent harness that supports
{option}`programs.claude-code.configDir`, so they are active in every Agent Skills.
project.
Selection is by derivation: pass skill derivations from the skills flake's Selection is by derivation: pass skill derivations from the skills flake's
`packages.<system>`, each of which carries its placement name as an `packages.<system>`, each of which carries its placement name as an
eval-time attribute. An empty list installs nothing. eval-time attribute. An empty list installs nothing.
Every skill is written as its own recursive {option}`home.file`, so this Every skill is written as its own recursive {option}`home.file`, so this
option composes with an operator's own {option}`programs.claude-code.skills` option composes with an operator's own harness-specific skills and with
and with other modules placing skills under the same directory rather than other modules placing skills under the same directory rather than claiming
claiming the tree. Placement is gated on the tree. Placement is gated on each harness' enable option.
{option}`programs.claude-code.enable`.
''; '';
}; };
# `agents` is an umbrella namespace with no shared enable, so the skills # `agents` is an umbrella namespace with no shared enable, so the skills
# feature self-gates. # feature self-gates.
# An empty list writes nothing, and files land only when Claude Code is # An empty list writes nothing, and files land only when a compatible harness
# enabled. # is enabled.
# Reading each skill's source realises its derivation, so gating on # Reading each skill's source realises its derivation, so gating on harness
# `claude-code.enable` also keeps that realisation off a host that installs # enablement also keeps that realisation off a host that installs nothing.
# nothing. config = lib.mkMerge [
config = lib.mkIf claudeCode.enable { (lib.mkIf claudeCode.enable {
# `configDir` comes from the Claude Code module, so skills land beside its # Each name is the derivation's eval-time `skillName`, so placement reads
# own wherever the operator points it. # no `$out` and needs no import-from-derivation.
# Each name is the derivation's eval-time `skillName`, so placement reads no # `recursive = true` materialises `skills/<name>/` as a directory of
# `$out` and needs no import-from-derivation. # per-file symlinks, which lets this module, the operator's own skills,
# `recursive = true` materialises `skills/<name>/` as a directory of per-file # and self-placing tool modules share one `skills/` tree.
# symlinks, which is what lets this module, the operator's own skills, and #
# self-placing tool modules share one `skills/` tree. # This deliberately does not feed the single-valued
# # `programs.claude-code.skills`, which would collide with an operator
# This deliberately does not feed the single-valued # already setting it.
# `programs.claude-code.skills`, which would collide with an operator already home.file = skillFiles claudeCode.configDir;
# setting it. })
home.file = lib.listToAttrs (
map (skill: { (lib.mkIf pi.enable {
name = "${claudeCode.configDir}/skills/${skill.skillName}"; home.file = skillFiles pi.configDir;
value = { })
source = skill; ];
recursive = true;
};
}) cfg.skills
);
};
} }