diff --git a/checks/home-manager-module.nix b/checks/home-manager-module.nix index 789acca..66df1a6 100644 --- a/checks/home-manager-module.nix +++ b/checks/home-manager-module.nix @@ -87,15 +87,38 @@ let programs.agents.skills = [ skillAlpha ]; }; - # Claude Code disabled, with a skill still selected. - claudeCodeOff = homeFiles { - programs.claude-code.enable = false; + # Pi enabled and Claude Code disabled. + piOnly = homeFiles { + programs.pi-coding-agent.enable = true; 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 { programs.claude-code.enable = true; + programs.pi-coding-agent.enable = true; programs.agents.skills = [ ]; }; in @@ -107,7 +130,10 @@ pkgs.runCommandLocal "skills-home-manager-module-check" attrSetSkills wholeDirSkills customConfigDir - claudeCodeOff + piOnly + bothHarnesses + customPiConfigDir + agentsOff emptySelection ; } @@ -142,13 +168,35 @@ pkgs.runCommandLocal "skills-home-manager-module-check" test ! -e "$customConfigDir/.claude/skills/sample-alpha" \ || fail "skill was placed under a hardcoded .claude, ignoring configDir" - echo "Claude Code disabled: no skill entry is written despite a selection" - test ! -e "$claudeCodeOff/.claude/skills/sample-alpha" \ - || fail "a skill was placed with claude-code disabled" + echo "Pi enabled: selected skills land in Pi's global skills directory" + test -f "$piOnly/.pi/agent/skills/sample-alpha/SKILL.md" \ + || 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" 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" '' diff --git a/home-manager-module.nix b/home-manager-module.nix index 816ea3d..1de3e45 100644 --- a/home-manager-module.nix +++ b/home-manager-module.nix @@ -1,5 +1,5 @@ -# Places operator-selected skills into Claude Code's skills directory, one per -# skill, for every project. +# Places operator-selected skills into each enabled agent's global skills +# directory, one per skill, for every project. { config, lib, @@ -8,6 +8,18 @@ let cfg = config.programs.agents; 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 { options.programs.agents.skills = lib.mkOption { @@ -15,49 +27,42 @@ in default = [ ]; example = lib.literalExpression "[ inputs.skills.packages.\${system}.some-skill ]"; description = '' - Skills to install globally, into the `skills/` subdirectory of - {option}`programs.claude-code.configDir`, so they are active in every - project. + Skills to install globally into every enabled agent harness that supports + Agent Skills. Selection is by derivation: pass skill derivations from the skills flake's `packages.`, each of which carries its placement name as an eval-time attribute. An empty list installs nothing. 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` - and with other modules placing skills under the same directory rather than - claiming the tree. Placement is gated on - {option}`programs.claude-code.enable`. + option composes with an operator's own harness-specific skills and with + other modules placing skills under the same directory rather than claiming + the tree. Placement is gated on each harness' enable option. ''; }; # `agents` is an umbrella namespace with no shared enable, so the skills # feature self-gates. - # An empty list writes nothing, and files land only when Claude Code is - # enabled. - # Reading each skill's source realises its derivation, so gating on - # `claude-code.enable` also keeps that realisation off a host that installs - # nothing. - config = lib.mkIf claudeCode.enable { - # `configDir` comes from the Claude Code module, so skills land beside its - # own wherever the operator points it. - # Each name is the derivation's eval-time `skillName`, so placement reads no - # `$out` and needs no import-from-derivation. - # `recursive = true` materialises `skills//` as a directory of per-file - # 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 already - # setting it. - home.file = lib.listToAttrs ( - map (skill: { - name = "${claudeCode.configDir}/skills/${skill.skillName}"; - value = { - source = skill; - recursive = true; - }; - }) cfg.skills - ); - }; + # An empty list writes nothing, and files land only when a compatible harness + # is enabled. + # Reading each skill's source realises its derivation, so gating on harness + # enablement also keeps that realisation off a host that installs nothing. + config = lib.mkMerge [ + (lib.mkIf claudeCode.enable { + # Each name is the derivation's eval-time `skillName`, so placement reads + # no `$out` and needs no import-from-derivation. + # `recursive = true` materialises `skills//` as a directory of + # per-file symlinks, which 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 + # already setting it. + home.file = skillFiles claudeCode.configDir; + }) + + (lib.mkIf pi.enable { + home.file = skillFiles pi.configDir; + }) + ]; }