--- spec: nix-skill-packaging --- ## What to build The foundational tracer bullet: a standalone Nix flake that packages each skill as an individually addressable derivation, plus the check that proves it. The flake scaffolding uses `flake-utils.lib.eachSystem` over `x86_64-linux`, `aarch64-linux`, `aarch64-darwin`, with `nixpkgs` tracking unstable. An explicit system list is passed rather than `eachDefaultSystem`, whose default set includes the now-dropped `x86_64-darwin`. The content tier recursively walks the `skills/` tree: any directory containing a `SKILL.md` is a skill (at any depth), and once found, its own subfolders are its assets rather than further skills. Directories without a `SKILL.md` are cosmetic organizational containers the walk descends through — the nesting never reaches the placement name and is not a selectable unit. Each discovered skill becomes an individually addressable derivation exposed at `packages..`, built by an exposed `lib.mkSkill` builder. There is no `packages..default`. A skill is addressed by its name (leaf directory / frontmatter `name`), independent of its source path. The derivation satisfies the skill-derivation contract: `$out` contains `SKILL.md` at its root plus any assets, and the derivation carries its name as an eval-time attribute (e.g. `pname` / a passthru) so placement can form `.../skills/` without import-from-derivation. Nothing in this tier references `.claude/skills` or anything Claude-Code-specific. Names must be globally unique across the whole tree; a collision detected during the discovery walk is a hard eval-time build error with a clear message, not a warning. Verified by the skill-build check under `nix flake check`, which builds every auto-discovered skill derivation and thereby transitively exercises the recursive walk, the `mkSkill` builder, the `SKILL.md`-at-`$out`-root contract, and the name-uniqueness hard error. Because the repo ships with no real skill content, this check drives fixture skills (à la gitea-axi's `runCommandLocal` fixtures), including a collision fixture that must fail the build. ## Acceptance criteria - [x] `flake.nix` iterates the three systems via `flake-utils.lib.eachSystem` (explicit list, not `eachDefaultSystem`) with `nixpkgs` unstable. - [x] The `skills/` tree is auto-discovered recursively: any directory containing a `SKILL.md` is a skill at any depth; its subfolders become its assets, not further skills. - [x] Directories without a `SKILL.md` are traversed as cosmetic containers only and never affect a skill's placement name. - [x] Each discovered skill is exposed at `packages..`; there is no `packages..default`. - [x] `lib.mkSkill` is exposed and turns a skill source directory into a conforming derivation. - [x] A built skill's `$out` contains `SKILL.md` at its root, and the derivation carries its name as an eval-time attribute (no import-from-derivation needed to read it). - [x] No content-tier derivation references `.claude/skills` or anything Claude-Code-specific. - [x] Two skills that resolve to the same name fail the build at eval with a clear collision message. - [x] `nix flake check` includes a skill-build check that builds every auto-discovered skill; a name-collision fixture makes it fail. ## Implementation Notes Files: `flake.nix` (scaffolding + wiring), `lib/mk-skill.nix` (builder), `lib/discover.nix` (recursive walk + uniqueness), `checks/skill-build.nix` with fixtures under `checks/fixtures/`, and an empty `skills/.gitkeep` root. - **Name source — leaf directory name, not frontmatter.** The spec's "What to build" phrases the name as "leaf directory / frontmatter `name`", but the acceptance criteria only ever require the leaf directory name (criteria 2 and 6). Discovery and `mkSkill` derive the name purely from the leaf directory (`mkSkill`'s `name` defaults to `builtins.baseNameOf src`). The two are equal by Claude Code convention; parsing `SKILL.md` YAML frontmatter at eval would add real complexity for no behavioural gain when they agree, so the frontmatter branch is deliberately not implemented. If divergence between directory name and frontmatter name ever needs enforcing, that is an additive validation for later. - **Eval-time name attribute.** Carried as `passthru.skillName` (in addition to the derivation's own `name`), so the integration tier can form `.../skills/` at eval time without import-from-derivation. - **Empty `skills/` root.** The repo ships no real skill content, so `skills/` holds only a `.gitkeep` and `packages.` is an empty set today. The `skill-build` check therefore drives fixture skills (a top-level skill, one under a cosmetic container with an asset subfolder, and one nested several containers deep) plus a separate two-skills-one-name collision fixture that `builtins.tryEval` confirms fails discovery at eval. - **Real skills also folded into `checks` (beyond the literal criteria).** Each real discovered skill is added to `checks.` alongside `skill-build`, so once skills land `nix flake check` builds each one and fails on a malformed skill (spec user story 17). This is empty today and is a natural extension of criterion 9's "builds every auto-discovered skill", not new scope. - **Integration tier (home-manager module, dev-shell shellHook) is intentionally absent** — it belongs to tasks 0002 and 0003. No `home-manager` flake input is added yet for that reason.