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.
This commit is contained in:
53
.claude/tasks/0001-content-tier-skill-packaging.md
Normal file
53
.claude/tasks/0001-content-tier-skill-packaging.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
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
|
||||
|
||||
- [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.<system>.<skill-name>`; there is no `packages.<system>.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/<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.
|
||||
Reference in New Issue
Block a user