2026-10-08-dendritic-niri-noctalia.md (21549B)
1 # Dendritic NixDaemon with Niri + Noctalia Implementation Plan 2 3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. 4 5 **Goal:** Turn `~/NixDaemon` into a flake-parts + import-tree (dendritic) flake and add a Niri + Noctalia desktop beside Hyprland + Caelestia, each switchable with one boolean. 6 7 **Architecture:** `flake.nix` becomes `flake-parts.lib.mkFlake { inherit inputs; } (import-tree ./modules)`; every file under `modules/` is a flake-parts module that declares `flake.nixosModules.<name>`, `flake.homeModules.<name>`, `flake.nixosConfigurations.nixos` or `perSystem.packages.<name>`, and everything is wired by name through `self`. Existing NixOS/home-manager module bodies move unchanged. Niri and Noctalia are wrapper-modules packages (`perSystem.packages.niri` / `.noctalia`) consumed by a gated `programs.niri` NixOS module. 8 9 **Tech Stack:** NixOS unstable, flake-parts, import-tree, `github:BirdeeHub/nix-wrapper-modules`, home-manager (as a NixOS module), niri 26.04, noctalia-shell 4.7.7. 10 11 **Spec:** `docs/superpowers/specs/2026-10-08-dendritic-niri-noctalia-design.md` 12 13 ## Global Constraints 14 15 - Module bodies are carried over unchanged; only the wrapping and relative paths change. 16 - Rosé Pine Main only (base `#191724`, rose `#ebbcba`, overlay `#26233a`); never Moon. 17 - import-tree imports every `*.nix` under `modules/`; a path containing `/_` is skipped. Non-module `.nix` files must be renamed with a leading underscore. 18 - New files are invisible to the flake until `git add`; every task ends with `git add -A`. No commits: the owner commits with jj. 19 - `bootstrap` configuration and the `nixpkgs-stable` input are deleted. 20 - Nothing is switched live until Task 8; `nvd diff` must show no removals before `nh os switch`. 21 - Working directory for every command: `~/NixDaemon`. 22 23 ## Review Focus 24 25 1. A home module referencing a data file by the old `../x` path evaluates to a missing-path error only when that option is used; the toplevel build in Task 5 exercises all of them. Pinned by Task 5 step 3. 26 2. `daemon.desktop.hyprland.enable = false` must drop the Hyprland session and Caelestia from the closure without an evaluation error (the Hyprland and Caelestia HM modules from `sharedModules` still exist and must stay inert). Pinned by Task 4 step 6. 27 3. Both switches `false` must fail evaluation with the assertion message, not build a system with no login session. Pinned by Task 4 step 7. 28 4. The Niri config must pass `niri validate` (the wrapper runs it at build time); a typo in a bind name fails the build, not the login. Pinned by Task 7 step 3. 29 5. Noctalia must start with Rosé Pine without a writable config dir (settings come from the store). Pinned by Task 6 step 4 (nested run shows the Rosé Pine bar). 30 31 --- 32 33 ### Task 1: Flake skeleton and inputs 34 35 **Files:** 36 - Modify: `flake.nix` 37 - Create: `modules/parts.nix` 38 - Delete: `hosts/bootstrap/default.nix` 39 40 **Interfaces:** 41 - Produces: inputs `flake-parts`, `import-tree`, `wrapper-modules`; flake-parts modules receive `{ self, inputs, ... }`; `perSystem` receives `{ pkgs, lib, self', ... }`; `flake.homeModules.*` exists (from home-manager's flake module); `wrappers` are reachable as `inputs.wrapper-modules.wrappers.<name>`. 42 43 - [ ] **Step 1: Rewrite `flake.nix`** 44 45 Keep the description and every existing input except `nixpkgs-stable`; add 46 ```nix 47 flake-parts.url = "github:hercules-ci/flake-parts"; 48 flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 49 import-tree.url = "github:vic/import-tree"; 50 wrapper-modules.url = "github:BirdeeHub/nix-wrapper-modules"; 51 ``` 52 Replace the whole `outputs` with 53 ```nix 54 outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules); 55 ``` 56 Carry the explanatory comments on the inputs over; drop the `bootstrap` comment block. 57 58 - [ ] **Step 2: Create `modules/parts.nix`** 59 60 ```nix 61 # modules/parts.nix — flake-parts wiring shared by every module under modules/. 62 { inputs, ... }: 63 { 64 systems = [ "x86_64-linux" ]; 65 imports = [ 66 inputs.home-manager.flakeModules.home-manager # flake.homeModules / homeConfigurations 67 inputs.wrapper-modules.flakeModules.default # flake.wrappers, perSystem.wrappers 68 ]; 69 } 70 ``` 71 72 - [ ] **Step 3: Delete `hosts/bootstrap/`, move the old `modules/workstation.nix` aside** 73 74 `git rm -r hosts/bootstrap`; `git mv modules/workstation.nix modules/_workstation.nix.old` (Task 4 turns it into `modules/features/workstation.nix`; the underscore keeps import-tree off it meanwhile). 75 76 - [ ] **Step 4: Lock and check the empty flake** 77 78 Run: `git add -A && nix flake lock && nix flake show 2>&1 | tail -20` 79 Expected: `flake.lock` gains `flake-parts`, `import-tree`, `wrapper-modules` and loses `nixpkgs-stable`; `nix flake show` prints an outputs tree with `homeModules`, `nixosModules` (empty) and `packages.x86_64-linux` (empty), no error. 80 81 ### Task 2: Host modules become `flake.nixosModules.laptop-*` 82 83 **Files:** 84 - Move: `hosts/laptop/*` → `modules/hosts/laptop/*` (with `hardware-configuration.nix` → `hardware.nix`, `uniwill-laptop/package.nix` → `uniwill-laptop/_package.nix`); `hosts/laptop/default.nix` → `modules/hosts/laptop/_old-default.nix` (consumed by Task 4, then deleted) 85 - Create: `scripts/wrap.sh` (helper; outside `modules/`, so import-tree never sees it) 86 87 **Interfaces:** 88 - Produces: `self.nixosModules.laptop-hardware`, `laptop-nvidia`, `laptop-ssd`, `laptop-nix-settings`, `laptop-sops`, `laptop-toolbox`, `laptop-fan-cli`, `laptop-fan-extras`, `laptop-fan-throttle-guard`, `laptop-uniwill`. Each is the unchanged NixOS module function `{ config, pkgs, lib, inputs, user, ... }: { … }`. 89 90 - [ ] **Step 1: Write the wrapping helper `scripts/wrap.sh`** 91 92 Usage: `scripts/wrap.sh FILE ATTR` rewrites FILE in place so that its leading comment block stays first, followed by 93 ``` 94 { ... }: 95 { 96 ATTR = 97 <original module, every line indented by two spaces>; 98 } 99 ``` 100 Algorithm: split at the first line that does not start with `#` (and is not blank); print the comment lines, then the header, then the rest with ` ` prefixed to non-empty lines, then `;` on its own line (indented two spaces) and `}`. Implement in bash + awk. 101 102 - [ ] **Step 2: Move the host files** 103 104 ```bash 105 mkdir -p modules/hosts/laptop 106 git mv hosts/laptop/hardware-configuration.nix modules/hosts/laptop/hardware.nix 107 for f in nvidia ssd nix-settings sops toolbox fan-cli fan-extras fan-throttle-guard uniwill-laptop; do git mv hosts/laptop/$f.nix modules/hosts/laptop/$f.nix; done 108 git mv hosts/laptop/default.nix modules/hosts/laptop/_old-default.nix 109 git mv hosts/laptop/uniwill-laptop modules/hosts/laptop/uniwill-laptop 110 git mv modules/hosts/laptop/uniwill-laptop/package.nix modules/hosts/laptop/uniwill-laptop/_package.nix 111 git mv hosts/laptop/fanfix hosts/laptop/stability_guard.py modules/hosts/laptop/ 112 rmdir hosts/laptop hosts 113 ``` 114 115 - [ ] **Step 3: Wrap each file** 116 117 `scripts/wrap.sh modules/hosts/laptop/<file>.nix flake.nixosModules.laptop-<name>` with names: `hardware`, `nvidia`, `ssd`, `nix-settings`, `sops`, `toolbox`, `fan-cli`, `fan-extras`, `fan-throttle-guard`, and `uniwill-laptop.nix` → `laptop-uniwill`. 118 119 - [ ] **Step 4: Fix the paths that moved relative to their targets** 120 121 - `modules/hosts/laptop/sops.nix`: `defaultSopsFile = ../../../secrets/secrets.yaml;` 122 - `modules/hosts/laptop/uniwill-laptop.nix`: `./uniwill-laptop/_package.nix` 123 - `fan-throttle-guard.nix` (`./fanfix`, `./stability_guard.py`) needs no change. 124 125 - [ ] **Step 5: Verify the module set evaluates** 126 127 Run: `git add -A && nix eval .#nixosModules --apply 'm: builtins.attrNames m'` 128 Expected: `[ "laptop-fan-cli" "laptop-fan-extras" "laptop-fan-throttle-guard" "laptop-hardware" "laptop-nix-settings" "laptop-nvidia" "laptop-sops" "laptop-ssd" "laptop-toolbox" "laptop-uniwill" ]` 129 130 ### Task 3: Home modules become `flake.homeModules.*` 131 132 **Files:** 133 - Move: `home/modules/*.nix` → `modules/home/*.nix`; `home/{hypr,caelestia,kitty,cheats,gpg,rmpc,mpv,yazi}` → `modules/home/…`; `home/default.nix` → `modules/home/default.nix` (rewritten) 134 135 **Interfaces:** 136 - Consumes: `osConfig` (home-manager passes the NixOS config to HM modules when run as a NixOS module). 137 - Produces: `self.homeModules.<name>` for `caelestia cheats dotfiles fan git gpg gtk hyprland media neovim prompt shell sops ssh terminal tools yazi`, and `self.homeModules.daemonsec` that imports all of them. 138 139 - [ ] **Step 1: Move files** 140 141 ```bash 142 mkdir -p modules/home 143 for f in home/modules/*.nix; do git mv "$f" modules/home/; done 144 for d in hypr caelestia kitty cheats gpg rmpc mpv yazi; do [ -e home/$d ] && git mv home/$d modules/home/$d; done 145 git mv home/default.nix modules/home/default.nix 146 rmdir home/modules home 147 ``` 148 149 - [ ] **Step 2: Wrap each module** 150 151 `scripts/wrap.sh modules/home/<name>.nix flake.homeModules.<name>` for every file except `default.nix`. 152 153 - [ ] **Step 3: Fix relative paths** (`../x` → `./x`) 154 155 - `hyprland.nix`: seven `../hypr/*.lua` → `./hypr/*.lua` 156 - `caelestia.nix`: `../caelestia/…` (five occurrences) → `./caelestia/…` 157 - `terminal.nix`: `../kitty/scrollback.lua` → `./kitty/scrollback.lua` 158 - `gpg.nix`: `../gpg/daemon-main.pub.asc` → `./gpg/…` 159 - `media.nix`: `../rmpc/…` (three) and `../mpv/shaders` → `./…` 160 - `cheats.nix`: `../cheats` → `./cheats` 161 - `yazi.nix`: `../yazi/flavors/rose-pine.yazi` → `./yazi/…` 162 - `sops.nix`: `../../secrets/secrets.yaml` → `../../secrets/secrets.yaml` is unchanged in depth (`modules/home/` is two levels below the root, as `home/modules/` was). Verify with `ls modules/home/../../secrets/secrets.yaml`. 163 164 - [ ] **Step 4: Gate the Hyprland-only modules on the NixOS switch** 165 166 In `modules/home/hyprland.nix` and `modules/home/caelestia.nix` the wrapped function becomes `{ config, pkgs, lib, inputs, osConfig, ... }:` and its body set is wrapped as `lib.mkIf osConfig.daemon.desktop.hyprland.enable { … }`. The `let` block above the set stays outside the `mkIf`. 167 168 - [ ] **Step 5: Rewrite `modules/home/default.nix`** 169 170 ```nix 171 # modules/home/default.nix — the user's home-manager configuration: every 172 # home module in this directory, by name. 173 { self, ... }: 174 { 175 flake.homeModules.daemonsec = { user, ... }: { 176 imports = with self.homeModules; [ 177 hyprland caelestia terminal tools shell dotfiles cheats sops neovim prompt fan ssh gpg git media yazi gtk 178 ]; 179 home = { username = user; homeDirectory = "/home/${user}"; stateVersion = "26.05"; }; 180 programs.home-manager.enable = true; 181 xdg.enable = true; 182 }; 183 } 184 ``` 185 Carry the one-line per-module comments from the old file onto the import list. 186 187 - [ ] **Step 6: Verify** 188 189 Run: `git add -A && nix eval .#homeModules --apply 'm: builtins.length (builtins.attrNames m)'` 190 Expected: `18` 191 192 ### Task 4: Features, desktop switches, host configuration 193 194 **Files:** 195 - Create: `modules/features/workstation.nix` (from `modules/_workstation.nix.old`), `modules/features/home-manager.nix`, `modules/features/desktop/options.nix`, `modules/features/desktop/hyprland.nix`, `modules/hosts/laptop/configuration.nix`, `modules/hosts/laptop/default.nix` 196 - Delete: `modules/hosts/laptop/_old-default.nix`, `modules/_workstation.nix.old` 197 198 **Interfaces:** 199 - Produces: `self.nixosModules.workstation`, `home-manager`, `desktop-options`, `desktop-hyprland`, `laptop`; `self.nixosConfigurations.nixos`; NixOS options `daemon.desktop.hyprland.enable`, `daemon.desktop.niri.enable` (bool, default true). 200 201 - [ ] **Step 1: `modules/features/workstation.nix`** 202 203 `git mv modules/_workstation.nix.old modules/features/workstation.nix`, then `scripts/wrap.sh modules/features/workstation.nix flake.nixosModules.workstation`. 204 205 - [ ] **Step 2: `modules/features/desktop/options.nix`** 206 207 Exactly the module in the spec's "The desktop switch" section, declared as `flake.nixosModules.desktop-options`. 208 209 - [ ] **Step 3: `modules/features/desktop/hyprland.nix`** 210 211 `flake.nixosModules.desktop-hyprland = { config, lib, pkgs, inputs, ... }: lib.mkIf config.daemon.desktop.hyprland.enable { imports … }` cannot carry `imports` inside `mkIf`, so structure it as 212 ```nix 213 { imports = [ inputs.hyprland.nixosModules.default ]; 214 config = lib.mkIf config.daemon.desktop.hyprland.enable { 215 programs.hyprland = { enable = true; withUWSM = true; xwayland.enable = true; }; 216 xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; 217 }; } 218 ``` 219 These two blocks are cut from `_old-default.nix` (the `##### Desktop` section) with their comments. 220 221 - [ ] **Step 4: `modules/features/home-manager.nix`** 222 223 `flake.nixosModules.home-manager = { inputs, user, ... }: { imports = [ inputs.home-manager.nixosModules.home-manager ]; home-manager = { useGlobalPkgs = true; useUserPackages = true; backupFileExtension = "hm-bak"; extraSpecialArgs = { inherit inputs user; }; sharedModules = [ inputs.hyprland.homeManagerModules.default inputs.caelestia-shell.homeManagerModules.default ]; users.${user} = self.homeModules.daemonsec; }; }` — the block from the old `flake.nix`, with the comment about Caelestia/Hyprland pins. 224 225 - [ ] **Step 5: `modules/hosts/laptop/configuration.nix` and `default.nix`** 226 227 `configuration.nix`: `flake.nixosModules.laptop = { config, pkgs, lib, user, ... }: { imports = with self.nixosModules; [ laptop-hardware laptop-fan-throttle-guard laptop-fan-extras laptop-uniwill laptop-nvidia laptop-ssd laptop-nix-settings laptop-toolbox laptop-sops laptop-fan-cli workstation home-manager desktop-options desktop-hyprland ]; daemon.desktop = { hyprland.enable = true; niri.enable = true; }; … }` where `…` is the body of `_old-default.nix` minus its `imports` and minus the two blocks moved in Step 3. Keep the file header comment and the per-import comments. (`desktop-niri` is added to this list in Task 7.) 228 229 `default.nix`: 230 ```nix 231 { self, inputs, ... }: 232 { 233 flake.nixosConfigurations.nixos = inputs.nixpkgs.lib.nixosSystem { 234 system = "x86_64-linux"; 235 specialArgs = { inherit inputs; user = "daemonsec"; }; 236 modules = [ self.nixosModules.laptop ]; 237 }; 238 } 239 ``` 240 Then `git rm modules/hosts/laptop/_old-default.nix`. 241 242 - [ ] **Step 6: Verify the system evaluates, and that the Hyprland switch is inert when off** 243 244 Run: `git add -A && nix eval .#nixosConfigurations.nixos.config.system.build.toplevel.drvPath` 245 Expected: a `/nix/store/…-nixos-system-nixos-….drv` path. 246 247 Run: `nix eval --impure --expr '(builtins.getFlake (toString ./.)).nixosConfigurations.nixos.extendModules { modules = [ ({ lib, ... }: { daemon.desktop.hyprland.enable = lib.mkForce false; }) ]; }' --apply 'c: [ c.config.programs.hyprland.enable c.config.home-manager.users.daemonsec.wayland.windowManager.hyprland.enable c.config.home-manager.users.daemonsec.programs.caelestia.enable ]'` 248 Expected: `[ false false false ]` (mkForce: the host file sets the switch explicitly; the lambda is parenthesised because it sits in a list) 249 250 - [ ] **Step 7: Verify the assertion** 251 252 Run: `nix eval --impure --expr '((builtins.getFlake (toString ./.)).nixosConfigurations.nixos.extendModules { modules = [ ({ lib, ... }: { daemon.desktop.hyprland.enable = lib.mkForce false; daemon.desktop.niri.enable = lib.mkForce false; }) ]; }).config.system.build.toplevel.drvPath' 2>&1 | grep -c 'enable at least one desktop'` 253 Expected: `1` 254 255 (`daemon.desktop.niri.enable` exists from Step 2 even before Task 7 wires it.) 256 257 ### Task 5: Build parity with today's system 258 259 **Files:** none 260 261 - [ ] **Step 1: Flake check** 262 263 Run: `nix flake check` 264 Expected: exits 0. 265 266 - [ ] **Step 2: Build and diff against the running system** 267 268 Run: `nh os build && nvd diff /run/current-system result` 269 Expected: the only lines are version-neutral (`No version or selection state changes.`) or additions; the `Removed packages` section is absent. If anything is removed, the port lost a module: find which `self.*` name is missing from an import list and fix before continuing. 270 271 - [ ] **Step 3: Home activation builds** 272 273 Run: `nix build .#nixosConfigurations.nixos.config.home-manager.users.daemonsec.home.activationPackage --no-link --print-out-paths` 274 Expected: a store path (this forces every `./hypr`, `./caelestia`, `./kitty`, `./cheats`, `./gpg`, `./rmpc`, `./mpv`, `./yazi` path to resolve). 275 276 ### Task 6: Noctalia package 277 278 **Files:** 279 - Create: `modules/features/desktop/noctalia.nix` 280 281 **Interfaces:** 282 - Produces: `self'.packages.noctalia` / `self.packages.x86_64-linux.noctalia` (wrapped `noctalia-shell`; `lib.getExe` gives the `noctalia-shell` binary; `bin/dump-noctalia-shell` also present). 283 284 - [ ] **Step 1: Write the module** 285 286 ```nix 287 { inputs, ... }: 288 { 289 perSystem = { pkgs, ... }: { 290 packages.noctalia = inputs.wrapper-modules.wrappers.noctalia-shell.wrap { 291 inherit pkgs; 292 settings = { … }; 293 }; 294 }; 295 } 296 ``` 297 `settings` is the attribute set listed in the spec's "Noctalia" section (`colorSchemes`, `bar`, `general`, `ui`, `wallpaper` with `directory = "/home/daemonsec/git/daemon-sec-dotfiles/home/.config/omarchy/backgrounds/rose-pine-dark"`, `location`, `appLauncher`, `idle`). Add a header comment: how to export changes made in the GUI (`dump-noctalia-shell`). 298 299 - [ ] **Step 2: Build** 300 301 Run: `git add -A && nix build .#noctalia --print-out-paths --no-link` 302 Expected: a store path; `ls $(…)/bin` lists `noctalia-shell` and `dump-noctalia-shell`. 303 304 - [ ] **Step 3: The generated settings select Rosé Pine** 305 306 Run: `nix eval --raw .#noctalia.generatedConfig | xargs -I{} jq -c .colorSchemes {}/settings.json` 307 Expected: `{"darkMode":true,"predefinedScheme":"Rosepine","useWallpaperColors":false}` 308 309 - [ ] **Step 4: Nested smoke test (manual, in the running Hyprland session)** 310 311 Run: `nix run .#noctalia` for ten seconds; a Rosé Pine bar appears at the top; Ctrl+C stops it. (Noctalia runs under Hyprland too, so this proves the package before Niri exists.) 312 313 ### Task 7: Niri package and NixOS module 314 315 **Files:** 316 - Create: `modules/features/desktop/niri.nix` 317 - Modify: `modules/hosts/laptop/configuration.nix` (add `desktop-niri` to imports) 318 319 **Interfaces:** 320 - Consumes: `self'.packages.noctalia` (Task 6). 321 - Produces: `self'.packages.niri`; `self.nixosModules.desktop-niri`. 322 323 - [ ] **Step 1: Write the module** 324 325 ```nix 326 { self, inputs, ... }: 327 { 328 perSystem = { pkgs, lib, self', ... }: { 329 packages.niri = inputs.wrapper-modules.wrappers.niri.wrap { 330 inherit pkgs; 331 settings = { … }; 332 }; 333 }; 334 flake.nixosModules.desktop-niri = { config, lib, pkgs, ... }: { 335 config = lib.mkIf config.daemon.desktop.niri.enable { 336 programs.niri = { enable = true; package = self.packages.${pkgs.stdenv.hostPlatform.system}.niri; }; 337 }; 338 }; 339 } 340 ``` 341 `settings` implements the spec's "Niri" section: `input`, `layout`, `prefer-no-csd`, `hotkey-overlay`, `xwayland-satellite.path`, `spawn-at-startup = [ (lib.getExe self'.packages.noctalia) ]`, `environment`, and `binds` exactly as the spec's table, with every program as `lib.getExe pkgs.<x>` (kitty, firefox, nautilus, obsidian, grim, slurp, wl-clipboard's `wl-copy`, brightnessctl, playerctl; `wpctl` from `pkgs.wireplumber`). Bind syntax follows the wrapper: `"Mod+Return".spawn = [ (lib.getExe pkgs.kitty) ]; "Mod+Q".close-window = null; "Mod+1".focus-workspace = 1; "Mod+Space".spawn-sh = "${noctalia} ipc call launcher toggle";`. The Noctalia binary is `let noctalia = lib.getExe self'.packages.noctalia; in`. 342 343 - [ ] **Step 2: Add `desktop-niri` to the laptop imports** in `modules/hosts/laptop/configuration.nix`. 344 345 - [ ] **Step 3: Build (this runs `niri validate` on the generated config)** 346 347 Run: `git add -A && nix build .#niri --print-out-paths --no-link` 348 Expected: a store path. A validation failure names the bad KDL line; fix the bind and rebuild. 349 350 - [ ] **Step 4: The system now carries the Niri session** 351 352 Run: `nix eval .#nixosConfigurations.nixos.config.services.displayManager.sessionPackages --apply 'l: map (p: p.name) l'` 353 Expected: a list naming both the Hyprland (uwsm) session package and the wrapped niri package. 354 355 - [ ] **Step 5: Nested smoke test (manual)** 356 357 Run: `nix run .#niri` inside Hyprland. A Niri window opens with the Noctalia bar; Alt is the modifier when nested: Alt+Return opens kitty, Alt+Space opens the launcher, Alt+Shift+E quits. 358 359 ### Task 8: Documentation, final build, hand-over 360 361 **Files:** 362 - Modify: `modules/home/cheats/nix.md` (the `layout` and `add` sections), `README.md` (its layout section) 363 364 - [ ] **Step 1: Update the cheat card and README** 365 366 In `nix.md` `## layout`, replace the path table with the new tree (flake.nix, modules/parts.nix, modules/hosts/laptop/*, modules/features/*, modules/home/*), and in `## add` point packages at `modules/home/tools.nix`, dotfiles at `modules/home/dotfiles.nix`, a new module at "a new `modules/home/<name>.nix` declaring `flake.homeModules.<name>` + one name in `modules/home/default.nix`", plus two lines: `daemon.desktop.niri.enable` / `hyprland.enable` in `modules/hosts/laptop/configuration.nix`, and `nix run ~/NixDaemon#niri` / `#noctalia`. Do the same edit in the README's layout section. 367 368 - [ ] **Step 2: Final parity build** 369 370 Run: `git add -A && nix flake check && nh os build && nvd diff /run/current-system result` 371 Expected: additions only (`niri`, `noctalia-shell`, `xwayland-satellite`, `xdg-desktop-portal-gnome`, `quickshell`, their closure); no `Removed packages` section. 372 373 - [ ] **Step 3: Hand over** 374 375 Stop here. Report the diff summary and the two manual steps left to the owner: `nh os switch`, then log out, pick "niri" in tuigreet, and `nh os rollback` if anything is wrong. The owner commits with jj.