Files
skills/.claude/tasks/0001-content-tier-skill-packaging.md
alexion 5b6c158d2e feat: package skills as per-skill Nix derivations (task 0001)
Add the content tier: a standalone flake that auto-discovers each skill
(a directory containing a SKILL.md, at any depth) and exposes it as an
individually addressable derivation built by lib.mkSkill. Directories
without a SKILL.md are descended through as cosmetic containers; once a
SKILL.md is found, that directory's subfolders are its assets, not
further skills. Skill names must be globally unique across the tree — a
collision is a hard eval-time error, not a warning.

A fixture-driven skill-build check under `nix flake check` exercises the
recursive walk, the builder, the SKILL.md-at-$out-root contract, the
eval-time name passthru, and the collision error. The repo ships no real
skill content yet, so packages.<system> is empty today.
2026-07-22 16:44:47 -04:00

5.4 KiB

spec
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.<system>.<skill-name>, built by an exposed lib.mkSkill builder. There is no packages.<system>.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/<name> 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

  • flake.nix iterates the three systems via flake-utils.lib.eachSystem (explicit list, not eachDefaultSystem) with nixpkgs unstable.
  • 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.
  • Directories without a SKILL.md are traversed as cosmetic containers only and never affect a skill's placement name.
  • Each discovered skill is exposed at packages.<system>.<skill-name>; there is no packages.<system>.default.
  • lib.mkSkill is exposed and turns a skill source directory into a conforming derivation.
  • 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).
  • No content-tier derivation references .claude/skills or anything Claude-Code-specific.
  • Two skills that resolve to the same name fail the build at eval with a clear collision message.
  • 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/<name> 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.<system> 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.<system> 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.