Files
gitea-axi/.claude/tasks/0047-flake-check-hm-module-composition.md
alexion 1468003f5b
All checks were successful
CI / test (22) (pull_request) Successful in 49s
CI / test (true, 24) (pull_request) Successful in 1m5s
CI / flake (pull_request) Successful in 2s
CI / test (22) (push) Successful in 49s
CI / test (true, 24) (push) Successful in 1m3s
CI / flake (push) Successful in 3s
feat(nix): check the home-manager module's composition (task 0047)
Add a `home-manager` flake input, its nixpkgs following this flake's, and a
`home-manager-module` check that evaluates the real module through
home-manager's standalone entry point and builds the home files derivation
under four operator configurations, asserting on the tree each produces:
the Skill coexisting with an operator's own skills in both the attribute-set
and whole-directory forms, the sibling-enable gate leaving no Skill when
Claude Code is off, and the hook merging into an operator's own SessionStart
list. This is the first automated proof of the module's composition (ADR 0021),
replacing verification by maintainer rebuild.
2026-07-20 20:10:15 -04:00

5.0 KiB

spec, blocked-by
spec blocked-by
hm-module-harness-integration 0046-reshape-hm-module-per-harness-toggle

What to build

Add the first automated proof of the module's composition, so that a change in home-manager or the Claude Code module that breaks the way the Skill is declared fails nix flake check rather than a maintainer rebuild weeks later.

A home-manager input is added to the flake, with its own nixpkgs following the flake's nixpkgs, so the module is checked against the same nixpkgs-and-home-manager pairing a consumer following this flake would get.

A flake check evaluates the actual module through home-manager's standalone configuration entry point and builds the resulting home files derivation under several configurations, asserting on the tree it produces. Building the home files derivation is sufficient — it is the file-linkage layer that actually decides whether two declarations collide — and needs neither the Claude Code binary nor a running agent. The check asserts on which files a generation contains, never on module internals (option values, store paths, the shape of the file mechanism).

The configurations exercised:

  • An operator declaring their own skills as an attribute set: the module's Skill lands alongside the operator's, each at its own name.
  • An operator declaring their own skills as a whole directory (path form): the module's Skill lands alongside the operator's directory contents. This is the case that guards the recursive-install coupling; a regression to a non-recursive path-form install fails here as a build-time file collision.
  • Claude Code disabled: no gitea-axi Skill entry is written. This guards the explicit sibling-enable gate introduced in the reshape.
  • The hook merges into an operator's own SessionStart hook list rather than replacing it.

This adds a module check alongside the existing package check in the flake's checks output rather than introducing a new kind of verification surface.

Acceptance criteria

  • The flake has a home-manager input whose nixpkgs follows the flake's nixpkgs; flake.lock is updated.
  • A new check under the flake's checks output evaluates the real module through home-manager's standalone configuration entry point and builds the home files derivation — no Claude Code binary or running agent required.
  • The attribute-set-skills configuration asserts the module's Skill and the operator's skill both land, each at its own name.
  • The whole-directory-skills (path form) configuration asserts the module's Skill lands alongside the operator's directory contents; a non-recursive path-form install would fail this as a build-time collision.
  • The Claude-Code-disabled configuration asserts no gitea-axi Skill entry is written.
  • A configuration with an operator's own SessionStart hook asserts the module's hook merges into that list rather than replacing it.
  • The check asserts on the generation's file tree only, not on option values or store paths.
  • nix flake check runs the new check across the flake's systems and passes.

Implementation Notes

The check lives in its own file, checks/home-manager-module.nix, imported from the flake's checks output beside the existing package check; the forAllSystems callback now destructures { pkgs, system } because the check needs pkgs to build fixtures and the derivation. home-manager is added as a flake input with inputs.nixpkgs.follows = "nixpkgs"; it is a check-only development input with no bearing on the package or the module a consumer imports, and the comment in flake.nix says so.

Each of the four configurations builds config.home-files — the home-manager file-linkage layer — and the final runCommandLocal asserts on that tree with test/grep only: skill files present or absent by path, and the two SessionStart commands present as quoted JSON string values in settings.json. The hook-merge assertion reads generated file content because that is the only place a list merge is observable; the spec's Testing Decisions name "the hook merges into the operator's own hooks" as a required assertion, so this stays within "assert on the file tree, not on option values or store paths".

On the cross-system criterion: nix flake check builds the check for the host system and passes, and omits the incompatible systems (aarch64-*) with a warning — identical per-system semantics to the pre-existing package check, which also builds only natively. The check is import-from-derivation-bearing (the Claude Code module reads the fixture skill directories at evaluation time), so evaluating a foreign system's check forces a cross-platform fixture build rather than skipping cleanly; this is not exercised by the default nix flake check and does not affect the native run.

Verified by nix flake check (passes, all outputs) and by inspecting each configuration's built home-files tree directly: the disabled-Claude-Code generation contains no .claude directory at all, and the merged-hook settings.json contains both hooks in the SessionStart array — confirming the assertions are not vacuous.