refactor: mirror module namespaces to their directories (task 0031) #23
@@ -24,6 +24,12 @@ _Avoid_: loader, importer, scanner
|
||||
The rule that every Module is imported unconditionally and guards its own body with `mkIf config.modules.<path>.enable`, so a Host reads as a checklist of `enable = true` flags.
|
||||
_Avoid_: feature flag, toggle, opt-in
|
||||
|
||||
**Namespace convention**:
|
||||
The rule that a Module's option path mirrors its directory path under `modules/`, so a file's location is its namespace.
|
||||
A file whose name matches its enclosing directory is that directory's index node, declaring the directory's own segment rather than a doubled one.
|
||||
A directory with no such file is a pure namespace prefix that carries no aggregate enable.
|
||||
_Avoid_: option tree, module path, config key
|
||||
|
||||
**admin identity**:
|
||||
The age identity held only in the operator's password manager, never committed, that is a recipient of every secrets file.
|
||||
It is the recovery path for any wiped machine and the credential that authorizes registering a new host.
|
||||
|
||||
24
.claude/adr/0004-module-namespace-mirrors-directory.md
Normal file
24
.claude/adr/0004-module-namespace-mirrors-directory.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
status: accepted
|
||||
---
|
||||
|
||||
# A Module's option namespace mirrors its directory
|
||||
|
||||
A Module's option path mirrors its directory path under `modules/`, so a file's location on disk is its namespace: `modules/agents/tools/gitea-axi.nix` declares `modules.agents.tools.gitea-axi`, and a subfolder like `agents/` or `tools/` is a real namespace segment, not a cosmetic grouping.
|
||||
A file whose name matches its enclosing directory is that directory's index node, declaring the directory's own segment — its `enable` or aggregator — rather than a doubled segment, so `desktop/hyprland/hyprland.nix` owns `modules.desktop.hyprland` while `desktop/hyprland/hypridle.nix` nests under it as `modules.desktop.hyprland.hypridle`.
|
||||
|
||||
We chose this nested-mirrors-directory shape over the previous flat names (`modules.claude-code`, `modules.gitea-axi`) because the flat scheme let a Module sit anywhere on disk regardless of its option path, so the tree stopped predicting where a namespace lived.
|
||||
Mirroring makes the two the single fact.
|
||||
A pure grouping directory (`agents/`, `tools/`) contributes a namespace segment but declares no aggregate `enable`: agents are enabled à la carte, so there is deliberately no `modules.agents.enable` that would turn on a bundle nobody wants as a unit.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Flat, location-independent names** (the prior state). Rejected: a Module's option path was unconstrained by its file's location, so the directory tree and the option tree drifted and neither could be read off the other.
|
||||
- **A subfolder as cosmetic grouping only**, with the option path skipping the folder (`agents/pi.nix` → `modules.pi`). Rejected: it reintroduces the same drift for grouped Modules and makes the folder a lie the namespace does not tell.
|
||||
- **An aggregator at every grouping level** (`modules.agents.enable`). Rejected: the agent Modules have no meaningful "all agents" bundle, and an aggregate enable there would invite turning on tools no Host wants together.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The `agents/` group carries `claude-code`, `pi`, `skills`, and `tools/gitea-axi`, each enabled individually under `modules.agents.*`, with no `modules.agents.enable`.
|
||||
- The index-file rule means adding a knob to an existing group (a new `desktop/hyprland/*.nix`) nests automatically without a naming decision, while a new top-level Module names its own segment.
|
||||
- The `skills` Module remains the one deliberate exception to the Enable convention — it wires unconditionally — which the namespace convention does not change.
|
||||
29
.claude/tasks/0031-module-namespace-tidy.md
Normal file
29
.claude/tasks/0031-module-namespace-tidy.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## What to build
|
||||
|
||||
Tidy the module tree so a Module's option namespace mirrors its directory under `modules/`, adopt that as a documented convention, group the agent-related Modules under a new `agents/` directory, bring `desktop/hyprland/` into conformance, and drop the obsolete reference Module.
|
||||
|
||||
The convention: a Module's option path mirrors its directory path, and a file whose name matches its directory is that directory's index node — it declares the directory's own segment (its `enable`/aggregator) rather than a doubled segment. A file `foo.nix` in directory `d/` declares `modules.<…>.d.foo`. A group directory with no matching index file contributes a namespace segment but no aggregate `enable`.
|
||||
|
||||
Applying it:
|
||||
|
||||
- **Agents grouping.** Relocate the agent Modules under `modules.agents.*`: `claude-code` (its whole directory, assets included) → `modules.agents.claude-code`; `pi` flattened from its directory to a single file → `modules.agents.pi`; the skills Module renamed from `agent-skills` → `agents/skills.nix`; and `gitea-axi` into an `agents/tools/` subgroup → `modules.agents.tools.gitea-axi`. `tools/` is a real namespace segment, not a cosmetic folder.
|
||||
- **No aggregators.** `agents/` and `tools/` are pure namespace prefixes — no `modules.agents.enable` or `modules.agents.tools.enable`. Agents are enabled à la carte.
|
||||
- **Skills stays enable-less.** The skills Module keeps its current behaviour (unconditionally wires `programs.agents.skills`, empty list); it is the one deliberate exception to the Enable convention, marked as intentional by a self-contained comment in the file.
|
||||
- **Desktop conformance.** Nest `hypridle` and `hyprlock` under `modules.desktop.hyprland.*` (matching the index-file rule, `hyprland.nix` being the index), and update `desktop.nix`'s aggregator to the new paths. The 13 flat `desktop/*.nix` Modules keep their `modules.desktop.<name>` names — broader semantic regrouping is explicitly out of scope for this task.
|
||||
- **Remove the example Module.** Delete `modules/example.nix`; the documented convention and the many real Modules supersede its teaching role.
|
||||
|
||||
Also update the one Host that carries these Modules and the live documentation, and record the convention in the domain model.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `CONTEXT.md` gains a `Namespace convention` glossary entry stating the directory-mirrors-namespace rule and the index-file rule, in glossary style (no implementation detail).
|
||||
- [x] An ADR (next number: `0004`) records the decision — nested-mirrors-directory over flat names, a subfolder as a real namespace segment, the index-file rule, and no `agents` aggregator — following the ADR format.
|
||||
- [x] Agent Modules resolve under `modules.agents.*`: `modules.agents.claude-code.enable`, `modules.agents.pi.enable`, and `modules.agents.tools.gitea-axi.enable` exist; `modules.claude-code`, `modules.pi`, and `modules.gitea-axi` no longer resolve.
|
||||
- [x] The skills Module lives at `agents/skills.nix` (renamed from `agent-skills.nix`), stays enable-less, still wires `programs.agents.skills`, and carries an in-file comment marking the Enable-convention exception as intentional.
|
||||
- [x] Neither `modules.agents.enable` nor `modules.agents.tools.enable` exists (pure namespace prefixes, no aggregator).
|
||||
- [x] `claude-code`'s assets (`CLAUDE.md`, `authentication.md`, `hooks/`, `skills/`) travel with the move and its relative references still resolve.
|
||||
- [x] `desktop/hyprland/`: `modules.desktop.hyprland.hypridle` and `modules.desktop.hyprland.hyprlock` resolve; the old `modules.desktop.hypridle`/`modules.desktop.hyprlock` no longer exist; `desktop.nix` enables the new paths; `modules.desktop.enable` still brings up the whole session.
|
||||
- [x] `modules/example.nix` is removed and `modules.example` no longer resolves.
|
||||
- [x] `hosts/neogaia/default.nix` uses the new option paths for claude-code, pi, and gitea-axi.
|
||||
- [x] The two live `CLAUDE.md` gotchas — the `gitea-axi` install line and the `claude-code` skill-source path — are updated to the new option/path; `.claude/tasks/*` are left unchanged as historical record.
|
||||
- [x] `nix flake check` builds `checks.x86_64-linux.neogaia` green (moved files staged so evaluation sees them).
|
||||
79
flake.lock
generated
79
flake.lock
generated
@@ -181,6 +181,24 @@
|
||||
"url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%3D0.5.0.tar.gz"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fromYaml": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@@ -299,6 +317,27 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager_4": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"skills",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784725727,
|
||||
"narHash": "sha256-J5+C9wsO0lhDyUalQzfplDbRjyHDYeEH5+9sdyXtwa8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "041a999e8c1c5b731913855909e68d30ca69b8e0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixos-hardware": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -441,10 +480,33 @@
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"nixvim": "nixvim",
|
||||
"skills": "skills",
|
||||
"sops-nix": "sops-nix",
|
||||
"stylix": "stylix"
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"home-manager": "home-manager_4",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784776947,
|
||||
"narHash": "sha256-IGVn6Z7dfeArObZvz7cLsGqJD8ipP2VkAZC/bOJgvJI=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "1caf18d72dd7cad7304401da0db6a6ba853d8827",
|
||||
"revCount": 5,
|
||||
"type": "git",
|
||||
"url": "https://git.alexion.dev/alexion/skills"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.alexion.dev/alexion/skills"
|
||||
}
|
||||
},
|
||||
"sops-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -478,7 +540,7 @@
|
||||
"nixpkgs"
|
||||
],
|
||||
"nur": "nur",
|
||||
"systems": "systems_2",
|
||||
"systems": "systems_3",
|
||||
"tinted-kitty": "tinted-kitty",
|
||||
"tinted-schemes": "tinted-schemes",
|
||||
"tinted-tmux": "tinted-tmux",
|
||||
@@ -529,6 +591,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"tinted-kitty": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Personal agent skills, packaged as per-skill derivations with a
|
||||
# home-manager module that places them under Claude Code's skills directory.
|
||||
skills = {
|
||||
url = "git+https://git.alexion.dev/alexion/skills";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# CachyOS kernel and binary cache. Pins its own nixpkgs so its cache stays
|
||||
# usable and the kernel is fetched from it.
|
||||
chaotic.url = "github:chaotic-cx/nyx/nyxpkgs-unstable";
|
||||
|
||||
20
modules/agents/skills.nix
Normal file
20
modules/agents/skills.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{ config, inputs, ... }:
|
||||
# Global agent skills from the skills flake, placed under the agent harness's
|
||||
# skills directory so they are active in every project. The flake's home-manager
|
||||
# module self-gates on the harness being enabled and installs nothing for an
|
||||
# empty selection, so a host without one carries no skills either way.
|
||||
#
|
||||
# Unlike every other module, this one declares no `enable` flag and wires
|
||||
# unconditionally, by design.
|
||||
# The flake's self-gating above already makes it inert where the harness is
|
||||
# absent, so a gate would guard nothing.
|
||||
let
|
||||
user = config.user.name;
|
||||
|
||||
# The skills installed globally, as derivations from the skills flake.
|
||||
skills = [ ];
|
||||
in
|
||||
{
|
||||
home-manager.sharedModules = [ inputs.skills.homeModules.default ];
|
||||
home-manager.users.${user}.programs.agents.skills = skills;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{ config, lib, ... }:
|
||||
# Reference example for the module shape: imported unconditionally, but inert
|
||||
# until a host sets its `enable` flag.
|
||||
let
|
||||
cfg = config.modules.example;
|
||||
in
|
||||
{
|
||||
options.modules.example.enable = lib.mkEnableOption "the reference example module";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.etc."skeleton-example".text = "This Module is enabled.\n";
|
||||
};
|
||||
}
|
||||
@@ -55,7 +55,7 @@ in
|
||||
ls = "eza -al --color=always --group-directories-first --icons=always";
|
||||
la = "eza -a --color=always --group-directories-first --icons=always";
|
||||
ll = "eza -l --color=always --group-directories-first --icons=always";
|
||||
lt = "eza -aT --color=always --group-directories-first --icons=always";
|
||||
lt = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
||||
"l." = "eza -a | grep -e '^\\.'";
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user