feat: package skills as per-skill Nix derivations (task 0001) #1

Merged
alexion merged 1 commits from task-0001-content-tier-skill-packaging into main 2026-07-22 17:05:26 -04:00
Owner

Task: .claude/tasks/0001-content-tier-skill-packaging.md (spec: nix-skill-packaging)

Summary

Adds the content tier: a standalone Nix flake that packages each skill as an individually addressable derivation.

  • flake.nix — hand-rolled forAllSystems over the three systems, nixpkgs unstable, no flake-utils/flake-parts (mirrors the sibling gitea-axi flake).
  • lib/discover.nix — recursive walk of skills/: any directory with a SKILL.md is a skill at any depth, its subfolders are then assets; directories without one are descended through as cosmetic containers. Globally-unique names enforced with a hard eval-time collision error.
  • lib/mk-skill.nixlib.mkSkill, the skill-derivation contract: $out holds SKILL.md at its root plus assets, and passthru.skillName carries the name at eval time (no import-from-derivation). Nothing here references .claude/skills or anything Claude-Code-specific.
  • Each discovered skill is exposed at packages.<system>.<name>; there is no default.
  • checks/skill-build.nix (+ fixtures) — the nix flake check seam: drives fixture skills through the whole tier and asserts a two-skills-one-name collision fixture fails discovery at eval via builtins.tryEval.

The repo ships no real skill content yet, so skills/ holds only a .gitkeep and packages.<system> is empty today.

Deviations

  • Name comes from the leaf directory, not SKILL.md frontmatter. The acceptance criteria only require the leaf directory name; the two are equal by Claude Code convention, and parsing frontmatter at eval would add complexity for no behavioural gain. Documented in the task's Implementation Notes.
  • Real discovered skills are also folded into checks.<system> (beyond the literal criteria) so nix flake check builds each real skill once they land and fails on a malformed one (spec user story 17). Empty today.
  • Integration tier (home-manager module, dev-shell shellHook) is intentionally out of scope — tasks 0002 and 0003.

Review

/review-uncommittedRisk: Low (worst factor), no correctness findings.

Risk

Overall: LOW

  • Blast radius: Low — all-new files, no existing callers, empty skills/ tree so nothing downstream builds yet.
  • Reversibility: Low — pure addition of a brand-new flake; deleting the files fully reverts. lib.mkSkill is a new API surface but has no consumers yet.
  • Test coverage: Low — the diff ships its own nix flake check driving fixtures for every discovery shape plus the collision hard-error.
  • Sensitive domain: Low — no auth, secrets, payments, or data migration; a cp -R into $out and a directory walk.
  • Size & complexity: Low — ~285 lines across small, single-purpose files; the only recursion is linear.
  • Runtime criticality: Low — dev/packaging tooling, no production hot path.

Unaddressed findings

None. The Standards axis raised one judgement-call (duplicated contract prose across flake.nix and checks/skill-build.nix) which is fixed in the diff. The Spec axis's only note — the unimplemented frontmatter-name branch — is within the letter of the acceptance criteria and recorded as a deliberate decision in the Implementation Notes; nothing left unaddressed.

Task: `.claude/tasks/0001-content-tier-skill-packaging.md` (spec: `nix-skill-packaging`) ## Summary Adds the **content tier**: a standalone Nix flake that packages each skill as an individually addressable derivation. - `flake.nix` — hand-rolled `forAllSystems` over the three systems, `nixpkgs` unstable, no flake-utils/flake-parts (mirrors the sibling `gitea-axi` flake). - `lib/discover.nix` — recursive walk of `skills/`: any directory with a `SKILL.md` is a skill at any depth, its subfolders are then assets; directories without one are descended through as cosmetic containers. Globally-unique names enforced with a hard eval-time collision error. - `lib/mk-skill.nix` — `lib.mkSkill`, the skill-derivation contract: `$out` holds `SKILL.md` at its root plus assets, and `passthru.skillName` carries the name at eval time (no import-from-derivation). Nothing here references `.claude/skills` or anything Claude-Code-specific. - Each discovered skill is exposed at `packages.<system>.<name>`; there is no `default`. - `checks/skill-build.nix` (+ fixtures) — the `nix flake check` seam: drives fixture skills through the whole tier and asserts a two-skills-one-name collision fixture fails discovery at eval via `builtins.tryEval`. The repo ships no real skill content yet, so `skills/` holds only a `.gitkeep` and `packages.<system>` is empty today. ### Deviations - **Name comes from the leaf directory, not `SKILL.md` frontmatter.** The acceptance criteria only require the leaf directory name; the two are equal by Claude Code convention, and parsing frontmatter at eval would add complexity for no behavioural gain. Documented in the task's Implementation Notes. - **Real discovered skills are also folded into `checks.<system>`** (beyond the literal criteria) so `nix flake check` builds each real skill once they land and fails on a malformed one (spec user story 17). Empty today. - Integration tier (home-manager module, dev-shell shellHook) is intentionally out of scope — tasks 0002 and 0003. ## Review `/review-uncommitted` — **Risk: Low** (worst factor), no correctness findings. ### Risk **Overall: LOW** - Blast radius: Low — all-new files, no existing callers, empty `skills/` tree so nothing downstream builds yet. - Reversibility: Low — pure addition of a brand-new flake; deleting the files fully reverts. `lib.mkSkill` is a new API surface but has no consumers yet. - Test coverage: Low — the diff ships its own `nix flake check` driving fixtures for every discovery shape plus the collision hard-error. - Sensitive domain: Low — no auth, secrets, payments, or data migration; a `cp -R` into `$out` and a directory walk. - Size & complexity: Low — ~285 lines across small, single-purpose files; the only recursion is linear. - Runtime criticality: Low — dev/packaging tooling, no production hot path. ### Unaddressed findings None. The Standards axis raised one judgement-call (duplicated contract prose across `flake.nix` and `checks/skill-build.nix`) which is fixed in the diff. The Spec axis's only note — the unimplemented frontmatter-name branch — is within the letter of the acceptance criteria and recorded as a deliberate decision in the Implementation Notes; nothing left unaddressed.
alexion reviewed 2026-07-22 16:18:52 -04:00
@@ -0,0 +2,4 @@
#
# Building this under `nix flake check` exercises the whole content tier at once.
# The repo ships no real skill content, so the check drives fixture skills under
# `./fixtures`, including a collision fixture that must fail discovery at eval.
Author
Owner

This is temporary state of the repo and should not be worked around. Keep the fixtures though, but the comments make it sound like this project will never have skills.

This is temporary state of the repo and should not be worked around. Keep the fixtures though, but the comments make it sound like this project will never have skills.
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +1,82 @@
{
description = "Personal Claude Code agent skills, packaged through Nix.";
Author
Owner

Remove reference to Claude Code.

Remove reference to Claude Code.
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +3,4 @@
# Tracks unstable to match the maintainer's system.
# This input governs standalone builds and `nix flake check` only.
# A consumer deduplicates by pointing it at their own nixpkgs.
Author
Owner

Bad comment

Bad comment
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +18,4 @@
"aarch64-darwin"
];
forAllSystems =
Author
Owner

Wasn't there a built in flake function for this?

Wasn't there a built in flake function for this?
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +35,4 @@
# Turns a skill source directory into a conforming derivation.
# Exposed as `lib.mkSkill` so a self-packaging tool repo can reuse the
# same contract.
Author
Owner

These comments should be on the function definition and not here?

These comments should be on the function definition and not here?
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +45,4 @@
# The skills this repo itself ships, empty today.
# A skill is added by creating `skills/<...>/<name>/SKILL.md`, and the walk
# finds it with no edit to this flake.
ownSkills = discoverSkills ./skills;
Author
Owner

bad variable name?

bad variable name?
alexion marked this conversation as resolved
alexion reviewed 2026-07-22 16:22:45 -04:00
alexion left a comment
Author
Owner

Reworked the comments substantively (not just the earlier style pass). Amended into the branch — replies inline below.

Reworked the comments substantively (not just the earlier style pass). Amended into the branch — replies inline below.
@@ -0,0 +2,4 @@
#
# Drives fixture skills through the whole content tier under `nix flake check`.
# The fixtures pin every discovery shape and the collision hard-error
# deterministically, independent of whichever real skills the repo carries.
Author
Owner

Reworded so the fixtures read as deliberate, permanent coverage — they pin every discovery shape and the collision hard-error deterministically, independent of whichever real skills the repo carries — rather than a workaround for today's empty tree. Fixtures kept as-is.

Reworded so the fixtures read as deliberate, permanent coverage — they pin every discovery shape and the collision hard-error deterministically, independent of whichever real skills the repo carries — rather than a workaround for today's empty tree. Fixtures kept as-is.
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +1,60 @@
{
description = "Personal agent skills, packaged through Nix.";
Author
Owner

Done. Dropped "Claude Code" from the flake description (now "Personal agent skills, packaged through Nix.") and removed the remaining Claude Code mentions from the content-tier comments too — the tier is tool-agnostic, so naming the tool even to disclaim it was out of place.

Done. Dropped "Claude Code" from the flake description (now "Personal agent skills, packaged through Nix.") and removed the remaining Claude Code mentions from the content-tier comments too — the tier is tool-agnostic, so naming the tool even to disclaim it was out of place.
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +3,4 @@
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
Author
Owner

Removed entirely. The nixos-unstable URL is self-documenting, and the rest was either obvious (a consumer overriding the input) or described downstream consumers.

Removed entirely. The `nixos-unstable` URL is self-documenting, and the rest was either obvious (a consumer overriding the input) or described downstream consumers.
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +18,4 @@
forAllSystems =
f:
nixpkgs.lib.genAttrs systems (
system:
Author
Owner

There's no forAllSystems in nixpkgs.libgenAttrs (used inside the helper) is the primitive it's built from. The named wrappers are flake-utils (eachDefaultSystem) and flake-parts (perSystem), both deliberately avoided per the spec to mirror the sibling gitea-axi flake and keep the input set minimal. Happy to pull in flake-utils for the shorter form if you'd rather — just say so and I'll switch both this repo and gitea-axi to stay consistent.

There's no `forAllSystems` in `nixpkgs.lib` — `genAttrs` (used inside the helper) is the primitive it's built from. The named wrappers are flake-utils (`eachDefaultSystem`) and flake-parts (`perSystem`), both deliberately avoided per the spec to mirror the sibling gitea-axi flake and keep the input set minimal. Happy to pull in flake-utils for the shorter form if you'd rather — just say so and I'll switch both this repo and gitea-axi to stay consistent.
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +35,4 @@
nixpkgs.lib.listToAttrs (
map (skill: {
inherit (skill) name;
value = mkSkill { inherit pkgs; inherit (skill) name src; };
Author
Owner

Agreed — removed it from the import site. The description already lives on the definition in lib/mk-skill.nix, so the mkSkill = import ./lib/mk-skill.nix; line now stands on its own.

Agreed — removed it from the import site. The description already lives on the definition in `lib/mk-skill.nix`, so the `mkSkill = import ./lib/mk-skill.nix;` line now stands on its own.
alexion marked this conversation as resolved
flake.nix Outdated
@@ -0,0 +45,4 @@
# No `default`: a catalog has no single default skill.
packages = forAllSystems ({ pkgs, ... }: skillPackages pkgs);
# Each discovered skill is folded in as its own check, so `nix flake check`
Author
Owner

Renamed ownSkillsdiscoveredSkills.

Renamed `ownSkills` → `discoveredSkills`.
alexion marked this conversation as resolved
alexion reviewed 2026-07-22 16:37:48 -04:00
alexion left a comment
Author
Owner

Adopted flake-utils per your call — amended into the branch.

Adopted flake-utils per your call — amended into the branch.
flake.nix Outdated
@@ -0,0 +18,4 @@
map (skill: {
inherit (skill) name;
value = mkSkill { inherit pkgs; inherit (skill) name src; };
}) discoveredSkills
Author
Owner

Switched to flake-utils.lib.eachSystem. To be precise about the earlier answer: there is no official forAllSystems/eachSystem in nixpkgs.lib (verified — genAttrs is the only primitive there), so this pulls in numtide/flake-utils as an input. It uses eachSystem with an explicit three-system list rather than eachDefaultSystem, because the default set includes x86_64-darwin, which recent nixpkgs dropped and whose legacyPackages now throws. nix flake check passes and the output shape is unchanged (packages.<system>.<name>, checks.<system>, system-independent lib). Updated the spec and task doc, which had recorded the hand-rolled/no-flake-utils decision. Left the sibling gitea-axi flake alone for now — happy to make the same change there separately if you want the two consistent.

Switched to `flake-utils.lib.eachSystem`. To be precise about the earlier answer: there is no official `forAllSystems`/`eachSystem` in `nixpkgs.lib` (verified — `genAttrs` is the only primitive there), so this pulls in `numtide/flake-utils` as an input. It uses `eachSystem` with an explicit three-system list rather than `eachDefaultSystem`, because the default set includes `x86_64-darwin`, which recent nixpkgs dropped and whose `legacyPackages` now throws. `nix flake check` passes and the output shape is unchanged (`packages.<system>.<name>`, `checks.<system>`, system-independent `lib`). Updated the spec and task doc, which had recorded the hand-rolled/no-flake-utils decision. Left the sibling gitea-axi flake alone for now — happy to make the same change there separately if you want the two consistent.
alexion marked this conversation as resolved
alexion added 1 commit 2026-07-22 16:51:41 -04:00
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.
alexion force-pushed task-0001-content-tier-skill-packaging from 5b6c158d2e to 8622b35f48 2026-07-22 16:51:41 -04:00 Compare
alexion merged commit 8622b35f48 into main 2026-07-22 17:05:26 -04:00
alexion deleted branch task-0001-content-tier-skill-packaging 2026-07-22 17:05:26 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alexion/skills#1