refactor: mirror module namespaces to their directories (task 0031)

Adopt the convention that a Module's option path mirrors its directory
under modules/, with an index file naming the directory's own segment.

- Group agent Modules under modules.agents.*: claude-code (whole
  directory), pi (flattened to a file), skills (renamed from
  agent-skills), and gitea-axi under an agents/tools/ subgroup. The
  agents/ and tools/ folders are pure namespace prefixes with no
  aggregator enable.
- Nest hypridle and hyprlock under modules.desktop.hyprland.*, with
  hyprland.nix as the index, and update the desktop aggregator.
- Remove the obsolete example Module.
- Record the convention in CONTEXT.md and ADR 0004, and update the
  neogaia host, the two live CLAUDE.md gotchas, and the skills Module's
  intentional Enable-convention exception comment.
This commit was merged in pull request #23.
This commit is contained in:
2026-07-23 19:21:12 -04:00
parent 02bb345fd7
commit e684ac481e
38 changed files with 64 additions and 13 deletions

View File

@@ -0,0 +1,53 @@
# Skills Lockfile
`.claude/skills-lock.yaml`, at the root of a project, tracks which library
skills (from `~/.claude/skills/library/`) have been copied into that
project's `.claude/skills/`, so [`setup-skills`](SKILL.md),
[`update-skills`](../update-skills/SKILL.md), and
[`remove-skills`](../remove-skills/SKILL.md) all agree on what's installed
without re-deriving it from the filesystem.
## Schema
A YAML list of entries, one per installed skill:
```yaml
- name: nbdev
hash: 3f2a9b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a
- name: terraform-conventions
hash: 9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a3f2a9b
```
- `name` — matches both the skill's directory name in the library
(`skills/library/<name>`) and its copied directory name in the project
(`.claude/skills/<name>`).
- `hash` — the output of `hash-dir.sh` run against that one skill's
directory contents, recorded at the moment it was last copied or
confirmed up to date. Never a hash of anything else — not the whole
project, not the whole library, just that one skill's own directory
tree.
## What a mismatch means
To classify a skill's state, compare three values: the lockfile's stored
`hash`, `hash-dir.sh` on the project's current copy
(`.claude/skills/<name>`), and `hash-dir.sh` on the library's current
source (`~/.claude/skills/library/<name>`).
| stored vs. project copy | stored vs. library source | meaning |
|--------------------------|----------------------------|--------------------------------------|
| match | match | nothing to do |
| match | differs | library moved on — safe to update |
| differs | match | project customized on purpose — leave it |
| differs | differs | conflict — report, don't touch |
## Writing to the lockfile
- Adding a skill: append a new `{name, hash}` entry.
- Applying a safe update: overwrite that entry's `hash` in place with the
library's current hash.
- Removing a skill: delete its entry entirely.
Never reorder or restructure existing entries beyond what an add, update,
or remove requires — this file is meant to diff cleanly in a project's
git history.

View File

@@ -0,0 +1,53 @@
---
name: setup-skills
description: Add relevant skills from the shared skills library to the current project.
disable-model-invocation: true
---
Adds opt-in, project-specific skills from `~/.claude/skills/library/` into
the current project's `.claude/skills/`, tracked in
`.claude/skills-lock.yaml` (see [LOCKFILE.md](LOCKFILE.md) for its schema).
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.
Note every skill name already listed — these are already installed and
must not be re-proposed.
2. List every skill under `~/.claude/skills/library/*/SKILL.md` and read
each one's `name` and `description`.
3. Inspect the current project (file tree, manifests like
`pyproject.toml`/`package.json`, file extensions present, etc.) and
judge which library skills — excluding ones already installed — seem
relevant, the same way you'd reason about any unfamiliar codebase.
Propose that shortlist to the user with your reasoning, one line per
skill. If the user asks to see the full catalog instead, list every
library skill (minus already-installed ones) with its description.
4. Let the user confirm, adjust, or pick freely from the full list.
5. For each confirmed skill:
- If `.claude/skills/<name>/` already exists in the project and is
*not* in the lockfile, skip it and tell the user why (a same-named
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 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).
6. Report what was added and what was skipped, and why.
Done when every confirmed skill is either copied and recorded in the
lockfile, or explicitly skipped with a stated reason.

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Deterministic recursive hash of a directory's file contents.
#
# Hashes relative paths, not absolute ones, so two directories with
# identical contents hash identically regardless of where they live on
# disk (needed to compare a project's copied skill against the library
# source it was copied from).
#
# Usage: hash-dir.sh <directory>
set -euo pipefail
if [ $# -ne 1 ]; then
echo "Usage: hash-dir.sh <directory>" >&2
exit 1
fi
dir="$1"
if [ ! -d "$dir" ]; then
echo "Not a directory: $dir" >&2
exit 1
fi
(cd "$dir" && find . -type f -print0 | sort -z | xargs -0 -r sha256sum) | sha256sum | awk '{print $1}'