docs(claude-code): teach the skill-management skills the Nix layout

~/.claude/skills is generated by home-manager: the directories are real
but every leaf file is a read-only symlink into the store. The skills
that author and install skills assumed it was an ordinary writable tree.

- craft-skill: personal skills are authored in modules/claude-code/skills
  and applied by a rebuild, never edited under ~/.claude/skills; writing
  there succeeds silently and strands the skill outside the repo.
- setup-skills, update-skills: copy out of the library with cp -rL and
  chmod -R u+w. A plain cp -r copies the symlinks, committing store paths
  into the project, and dereferenced files keep the store's read-only mode.
- craft-skill also staged through `dot add`, a fish function this repo no
  longer carries; plain git add replaces it.
This commit was merged in pull request #1.
This commit is contained in:
2026-07-19 08:34:22 -04:00
parent bdb6f01934
commit 7809e079e3
4 changed files with 20 additions and 6 deletions

View File

@@ -40,6 +40,10 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
- The remote is self-hosted Gitea (`git.alexion.dev`), driven with `gitea-axi` rather than `tea`; `gh` is not installed.
`gitea-axi` resolves the repository from the `origin` remote and takes credentials from the `axi` tea login, so both are implicit inside a checkout.
`tea` remains installed only as that credential source.
- `~/.claude/skills` is generated by home-manager with `recursive = true`, so the directories are real and writable but every leaf file is a read-only symlink into the store.
Editing a skill in place fails; its source is `modules/claude-code/skills/<name>/` here, applied by a rebuild.
Creating a new file under `~/.claude/skills/` succeeds silently and is the trap — it stays outside the repo and reaches no other machine.
Copying out of that tree needs `cp -rL` plus `chmod -R u+w`: a plain `cp -r` copies the symlinks, putting store paths into the destination, and dereferenced files keep the store's read-only mode.
- nixpkgs `vimPlugins.nord-nvim` is `shaunsingh/nord.nvim` (no `require("nord").setup()`); the config wants `gbprod/nord.nvim`, which is packaged as `vimPlugins.gbprod-nord`.
- nixpkgs `vimPlugins.nvim-treesitter` tracks the rewritten `main` branch: there is no `require("nvim-treesitter.configs").setup{ensure_installed,highlight,indent}`. Under nixvim, use `plugins.treesitter` with `highlight.enable`/`indent.enable` and `grammarPackages = with config.programs.nixvim.plugins.treesitter.package.builtGrammars; [ ... ]` — the module's own `package.builtGrammars`, **not** `pkgs.vimPlugins.nvim-treesitter.*` (whose query files can mismatch). The module targets the main branch and enables features via neovim-native APIs (`vim.treesitter.start()`, `require'nvim-treesitter'.indentexpr()`).
- Neovim is configured via **nixvim** (flake input `nixvim`, consumed as `inputs.nixvim.homeModules.nixvim` added to `home-manager.sharedModules`, config under `home-manager.users.<user>.programs.nixvim`). `nixvim.inputs.nixpkgs.follows = "nixpkgs"` is set; nixvim then emits a benign eval warning that its pinned nixpkgs differs from the followed one — builds and runs fine, do not "fix" it by dropping the follows.

View File

@@ -23,11 +23,11 @@ If the request describes a new workflow, capability, or repeated manual process
- Is there already a word — in your prompts, docs, or codebase — that names this behavior? Reach for that **leading word** before coining one.
Done when every axis above has an answer, or the user says to just draft something and iterate.
2. **Write the draft.** First decide where it lives: project-local `.claude/skills/` if the workflow is tied to this one repo, `~/.claude/skills/` if it's general-purpose across projects. Then follow the **information hierarchy**: steps for what the agent does in order, in-file **reference** for facts every branch needs, and disclose the rest behind a pointer — to a sibling file, or to the existing project docs identified in step 1 rather than restating them. Done when every branch from step 1 has somewhere to live, and no sentence fails the no-op test in isolation (see `No-Op` in GLOSSARY.md).
2. **Write the draft.** First decide where it lives: project-local `.claude/skills/` if the workflow is tied to this one repo, the personal set if it's general-purpose across projects. The personal set is not authored in `~/.claude/skills/` — that tree is generated, and every file under it is a read-only symlink into the Nix store. Write it in the dotfiles repo at `modules/claude-code/skills/<name>/` and rebuild to make it live. Creating files directly under `~/.claude/skills/` looks like it works, because the directories themselves are writable, but the result is untracked by the repo and reaches no other machine. Then follow the **information hierarchy**: steps for what the agent does in order, in-file **reference** for facts every branch needs, and disclose the rest behind a pointer — to a sibling file, or to the existing project docs identified in step 1 rather than restating them. Done when every branch from step 1 has somewhere to live, and no sentence fails the no-op test in isolation (see `No-Op` in GLOSSARY.md).
## Audit an existing skill
1. **Locate it.** Check the current project's `.claude/skills/`, then `~/.claude/skills/`, then `~/.claude/skills/library/`, in that order; ask if the name is ambiguous across locations. If it's tracked in a project's `skills-lock.yaml`, mention that editing it here will make it read as locally-customized to `update-skills` — confirm that's actually intended rather than editing the library source.
1. **Locate it.** Check the current project's `.claude/skills/`, then `~/.claude/skills/`, then `~/.claude/skills/library/`, in that order; ask if the name is ambiguous across locations. A hit under `~/.claude/skills/` is a read-only symlink and cannot be edited in place: its source is the dotfiles repo, at `modules/claude-code/skills/<name>/` for a personal skill or `modules/claude-code/skills/library/<name>/` for a library one. Edit there and rebuild. If it's tracked in a project's `skills-lock.yaml`, mention that editing it here will make it read as locally-customized to `update-skills` — confirm that's actually intended rather than editing the library source.
2. **Apply the checklist.** Read the skill and its disclosed files, then check each against GLOSSARY.md, quoting the offending line for anything that fails:
- **Premature completion** — is each completion criterion checkable, and does it demand what the step actually needs?
@@ -44,6 +44,6 @@ If the request describes a new workflow, capability, or repeated manual process
1. Propose one realistic test prompt — reflecting the trigger phrasing gathered (draft) or the skill's existing purpose (audit) — and get it confirmed or adjusted before spending a run on it.
2. Spawn one subagent: give it the skill's path and the confirmed prompt, have it attempt the task using the skill, and report back what happened — including anywhere it hesitated, misread the skill, or did something unexpected.
3. Re-read the draft/rewrite against GLOSSARY.md's failure modes in light of that run, and fix whatever either pass turned up. If the fix is substantial, repeat from step 1; otherwise it's done.
4. Stage the specific changed or created paths — one path per file, never a wildcard — with the host project's own staging convention: plain `git add <path>` normally, or e.g. `dot add <path>` in this dotfiles setup (wrap as `fish -c "dot add <path>"` if the invoking shell isn't fish — `dot` is a fish function, not a binary on `$PATH`). Do not commit; that's left to the user.
4. Stage the specific changed or created paths — one path per file, never a wildcard — with `git add <path>`. Do not commit; that's left to the user.
Done when the subagent's run succeeded without confusion on the confirmed prompt, the checklist raised nothing outstanding, and every changed path is staged.

View File

@@ -10,6 +10,12 @@ the current project's `.claude/skills/`, tracked in
Only ever adds — checking already-installed skills for updates is
[`update-skills`](../update-skills/SKILL.md)'s job, not this one's.
The library is a tree of read-only symlinks into the Nix store, so every
copy out of it must dereference (`cp -rL`) and then restore write
permission (`chmod -R u+w`). A plain `cp -r` copies the symlinks
themselves, putting store paths into the project that break on any other
machine.
## Steps
1. Read `.claude/skills-lock.yaml` in the current project, if it exists.
@@ -35,7 +41,8 @@ Only ever adds — checking already-installed skills for updates is
skill already lives there and isn't tracked — remove or rename it
first if they want the library version).
- Otherwise, copy `~/.claude/skills/library/<name>/` to
`.claude/skills/<name>/` in the project, run
`.claude/skills/<name>/` in the project with
`cp -rL` followed by `chmod -R u+w`, run
`~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/<name>`,
and append `{name, hash: <output>}` to `.claude/skills-lock.yaml`
(create the file, an empty YAML list, if it doesn't exist yet).

View File

@@ -36,8 +36,11 @@ skill that isn't already there — that's
already included an explicit go-ahead argument (e.g. `-y`, `yes`), in
which case apply them without asking. Applying means: delete
`.claude/skills/<name>/` entirely and copy
`~/.claude/skills/library/<name>/` in its place, so no file the project
copy had but the library no longer has can survive — then recompute its
`~/.claude/skills/library/<name>/` in its place with `cp -rL` followed
by `chmod -R u+w` (the library is read-only symlinks into the Nix store;
a plain `cp -r` would put store paths into the project), so no file the
project copy had but the library no longer has can survive — then
recompute its
hash and overwrite that entry's `hash` in `.claude/skills-lock.yaml` in
place.