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.
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.nixiterates the three systems viaflake-utils.lib.eachSystem(explicit list, noteachDefaultSystem) withnixpkgsunstable.- The
skills/tree is auto-discovered recursively: any directory containing aSKILL.mdis a skill at any depth; its subfolders become its assets, not further skills. - Directories without a
SKILL.mdare 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 nopackages.<system>.default. lib.mkSkillis exposed and turns a skill source directory into a conforming derivation.- A built skill's
$outcontainsSKILL.mdat 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/skillsor anything Claude-Code-specific. - Two skills that resolve to the same name fail the build at eval with a clear collision message.
nix flake checkincludes 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 andmkSkillderive the name purely from the leaf directory (mkSkill'snamedefaults tobuiltins.baseNameOf src). The two are equal by Claude Code convention; parsingSKILL.mdYAML 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 ownname), 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, soskills/holds only a.gitkeepandpackages.<system>is an empty set today. Theskill-buildcheck 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 thatbuiltins.tryEvalconfirms fails discovery at eval. -
Real skills also folded into
checks(beyond the literal criteria). Each real discovered skill is added tochecks.<system>alongsideskill-build, so once skills landnix flake checkbuilds 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-managerflake input is added yet for that reason.