NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

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.