--- spec: nix-skill-packaging blocked-by: 0001-content-tier-skill-packaging --- ## What to build The global (every-project) placement output: a home-manager module that installs an operator's selected skills into `~/.claude/skills/`, plus the check that proves its composition. Exposed as `homeModules.default`. The operator-facing option is `programs.agents.skills`, a `listOf package` with default `[]` (an empty list is a no-op). `agents` is deliberately an umbrella namespace with no shared `programs.agents.enable` — each sub-feature self-gates so the namespace stays a clean, mergeable surface another repo could extend. Selection is by derivation: consumers pass skill derivations pulled from `packages.`, not name strings. For each selected skill the module writes an individual `home.file` at `${claude-code.configDir}/skills/` with `source = ` and `recursive = true`, gated on `programs.claude-code.enable`, reading `configDir` from the claude-code module. `recursive = true` is a hard requirement: it materializes `.../skills//` as a real directory of per-file symlinks so this module, the operator's own skills declarations, and self-placing tool modules coexist under one `skills/` tree. The module deliberately does not feed the single-valued `programs.claude-code.skills` option, which would collide with an operator already setting it. Verified by the home-manager-module composition check under `nix flake check`: it instantiates `homeModules.default` under a sample home-manager configuration selecting a couple of skills with `programs.claude-code` enabled, builds the resulting home-files derivation, and asserts the per-skill `home.file`, `recursive = true`, the `claude-code.enable` gate, and `configDir` sourcing all compose as intended (direct analogue of gitea-axi's home-manager-module check). ## Acceptance criteria - [x] `homeModules.default` is exposed. - [x] It defines `programs.agents.skills` as `listOf package` with default `[]`, and an empty list installs nothing. - [x] There is no shared `programs.agents.enable`; the skills feature self-gates. - [x] Selection is by derivation (skills pulled from `packages.`), not by name string. - [x] Each selected skill is written as an individual `home.file` at `${claude-code.configDir}/skills/` with `recursive = true`, sourcing `configDir` from the claude-code module. - [x] Placement is gated on `programs.claude-code.enable`; with it off, no skill files are written. - [x] The module does not set `programs.claude-code.skills`. - [x] The module composes with an operator's own skills declarations and self-placing tool modules under one `skills/` tree without collision. - [x] `nix flake check` includes a home-manager-module composition check that builds the home-files derivation for a sample selection and asserts the above. ## Implementation Notes Files: `home-manager-module.nix` (the module, at the repo root, mirroring gitea-axi's placement), `checks/home-manager-module.nix` (the composition check), and `flake.nix` wiring (a `homeModules` output plus the check registration and the new `home-manager` input). - **Placement name comes from `skillName`.** The module reads each derivation's `passthru.skillName` — the eval-time name attribute task 0001 established for exactly this purpose — to form `.../skills/` without import-from-derivation. - **`homeModules` exposes an `agents-skills` alias beside `default`.** The criteria only require `homeModules.default`; the named alias is an additive convenience mirroring gitea-axi's `homeModules` shape, and `default` points at it. - **`home-manager` flake input follows this flake's nixpkgs.** It exists solely so `nix flake check` can evaluate the module against real home-manager; a consumer importing the module supplies their own home-manager and pkgs, so the input has no bearing on what they get. - **Recursive placement is proven by the entry's type, not by file existence.** A `test -f skills//SKILL.md` follows symlinks and cannot distinguish a recursive per-file tree from one opaque symlink over the whole skill, so the check asserts the entry is a real directory (`test -d` and `! -L`). A mutation to `recursive = false` fails the check. - **A non-default `configDir` scenario was added during review.** The other scenarios all run at the default `configDir`, so a module hardcoding `.claude/skills/` would have passed them identically. One configuration now sets a custom `configDir` and asserts placement follows it, closing the "configDir sourcing" leg of the check. A mutation hardcoding `.claude` fails the check.