Three findings from verifying the #41 merge against the live box with `deploy.sh --check`. One commit because they share the deploy tier, its documentation, and the first test file that tier ever had. **1. Generated palette output was tracked, and sync would have restored it over a live theme.** `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl` (`b3d3df4`, 2026-10-18) sat byte-identical to the live file — harmless while the palette held, and `--check` was already proposing `ADOPT … noctalia.kdl`. The moment the theme changed, the next sync would have quarantined the live file and reinstated 2026-10-18's colours. `deploy.sh` now refuses the class outright (`SKIP … (generated theme output)`), `.gitignore` carries the matching patterns so it cannot be re-added, and the file is untracked. **2. A `zz` override older than the file it overrides.** `dotfiles/zz-blankpad/niri/…/ layout.kdl` held focus ring `width 2` / `#e01b24` (`7b7dbf5`, 2026-10-17) while base absorbed `b3d3df4` (2026-10-18): `width 4`, `#ff8c00`. Stow order makes `zz-*` win, and a cross-package owner change relinks *silently* — no quarantine, no warning. Live was linked to base, the newer intent, so `--check` printed `RELINK … layout.kdl` every run and one unattended `deploy.sh` would have reverted the amber focus ring. The duplicate is deleted; base owns the file. **3. Two topologies I had asserted and not measured.** The media law and the `.gitignore` comment both said `~/.local/share/noctalia/wallpapers` symlinks into the repo, so deleting the tracked copies would blank the desktop. Measured: it is a real directory of real files that Noctalia owns; the repo held a duplicate, and the #41 merge deleted that duplicate without the desktop flickering. The correct general law replaces the false one — untracking a path deletes the file in every other checkout, `git rm --cached` on the branch notwithstanding — and the restore procedure now points at the app's directory, which `deploy.sh` does not manage. The guard is `tests/deploy.bats`, black-box over exit status, stdout, the symlink shape under `$HOME`, and the quarantine directory. Before the change: test 1 and test 2 fail, test 2 because the live palette really is quarantined and reverted. After: 4/4, including the control that an ordinary identical config is still adopted — an exclusion nobody can turn off is just a broken tool. Verification: `bats tests/` 66/66, `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0 issues, and live `deploy.sh --check` on blankpad reports `SKIP … (generated theme output)` with `actions=0` instead of `ADOPT … noctalia.kdl`. The real `deploy.sh` run that clears the remaining `RELINK` lines is deliberately left until this is in main — running it from a worktree would repoint live symlinks into a checkout the heartbeat reaps. Refs #42
7.4 KiB
type, title, description, tags, timestamp, verified
| type | title | description | tags | timestamp | verified | ||||
|---|---|---|---|---|---|---|---|---|---|
| reference | Dotfiles deployment tier | How dotfiles/ packages become live configuration — stow-equivalent symlinks, host override packages, and the two laws about theme media and systemd enable symlinks. |
|
2026-10-20 | 2026-10-20 |
Dotfiles deployment tier
dotfiles/ holds home-relative trees and scripts/deploy.sh is a stow-equivalent: the
repo checkout is the deployment, and deploying replaces live files with symlinks into
the repo, so git pull + deploy.sh is the whole propagation story. One directory per
tool (niri, ghostty, gtk-3.0, systemd-user, …); a package named zz-$(hostname)
is stowed last and its files replace symlinks coming from base packages.
| Mode | Effect |
|---|---|
deploy.sh --check |
dry run — adopts identicals, reports what would be linked, lists divergents, writes nothing. Run this first, always |
deploy.sh (sync) |
adopt identicals, link new, quarantine divergents into .quarantine/ and replace them |
deploy.sh --adopt |
like sync, but divergents are reported and skipped rather than quarantined |
Two package shapes are not symlinks: dconf/dump.txt is loaded with dconf load /, and
systemd-user units get daemon-reload plus enable --now only when their ExecStart
binary resolves on the current machine, so a unit belonging to another box stays inert
instead of crash-looping.
The tracked unit is the file, not the directory. A live directory can be a real
directory and still be fully tracked, because every file inside it is a symlink into the
repo (niri is 9 tracked / 1 box-only; gtk-3.0 is 3/0). Any audit that classifies by
directory mislabels the tree — classify per file with readlink -f. The live census and
its classifier command are in home-config-census.
Law: media the theme pipeline consumes is content, not config
Wallpapers live at dotfiles/noctalia/.local/share/noctalia/wallpapers/ and are
gitignored. They were tracked until 2026-10-20 — 23 files, 80 MiB — which meant every
clone of a configuration repo carried a media library no diff can review, and the
payload only ever grows because deleting a tracked blob from the tree does not delete it
from history. The topology is not what one would guess: Noctalia
keeps its own library at ~/.local/share/noctalia/wallpapers/ as a real directory of
real files, so the tracked tree was a duplicate of it and never its source — measured
2026-10-20, when the merge of #41 deleted the repo copies and the desktop did not
flicker. Two consequences to know before untracking media: the copy that survives belongs
to the application, not to the repo; and untracking deletes the file in every other
checkout, git rm --cached on the branch notwithstanding.
Restore on a fresh box: the library lives in ~/.local/share/noctalia/wallpapers/, a path
deploy.sh does not manage. Every filename is wallhaven_<id>.<ext> and the ID is the
whole address — curl -L -o <name> "https://wallhaven.cc/w/<id>" into that directory puts
the set back. Losing it is an acceptable outcome; 80 MB per clone forever is not.
Law: never systemctl --user enable a unit from a worktree
enable writes ~/.config/systemd/user/default.target.wants/<unit> as a symlink to the
unit path it was handed, and that path is absolute. The heartbeat reaps wt/* worktrees
older than seven days (live-sync), so a unit enabled from a worktree turns
into a dangling symlink while systemctl --user is-enabled still answers enabled — the
unit is dead and the status lies about it. Found on 2026-10-20:
blankpad-desk-autoswitch.service pointed into the reaped wt/de-redesign-20261009, so
desk auto-switching had been silently off since the reap.
Check the link target before the unit file:
ls -l ~/.config/systemd/user/default.target.wants/
# every target must resolve; a dangling one is a reaped worktree, not a config bug
Fix is to relink into the deployment checkout (~/.omp/dotfiles/…), systemctl --user daemon-reload, then confirm with is-active — is-enabled proves nothing.
Law: a generated file must never be a required include
~/.config/niri/config.kdl ends with include "noctalia.kdl" — no optional=true.
That file is noctalia's palette output (#ff8c00, #f43841, one re-stamp
2026-10-18 17:36:27), so it is machine state and correctly untracked. But on a fresh
box deploy.sh links config.kdl first, and niri then refuses to start until noctalia
has run once and written the file — the compositor is bricked by the order of operations,
not by a bad config. Close it at the include, which survives someone skipping the theme
pipeline: mark generated includes optional=true, as the display.kdl and
switch-events.kdl lines in the same file already are.
Desk choice is live state, never config
~/.config/niri/cfg/display.kdl is a relative symlink to whichever profile is active
(desk-1-home.kdl, desk-2-portable.kdl), written by desk.sh. Both profiles are
tracked in dotfiles/zz-blankpad/; the pointer is not, because which desk the box sits on
is not a fact about the repo. A fresh box therefore starts with no output configuration —
config.kdl includes display.kdl with optional=true, so niri comes up usable but with
default monitors until desk.sh is run once.
Law: generated theme output is never deployed
noctalia rewrites palette output in place — noctalia.*, themes/noctalia*,
colors/noctalia*, noctalia-* (the classes the census
proves by same-millisecond mtime). A tracked copy of one is a stale snapshot of a colour
scheme, and the failure is quiet: dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl
(commit b3d3df4, 2026-10-18) sat byte-identical to the live file for two days, harmless,
while --check cheerfully proposed ADOPT … noctalia.kdl. The moment the palette changed,
the next sync would have quarantined the live file and restored 2026-10-18's colours —
a tool silently reverting a user-visible theme choice.
deploy.sh now refuses the class (SKIP … (generated theme output)), .gitignore carries
the matching patterns so it cannot be re-added, and tests/deploy.bats pins both
directions: generated output is never adopted, and an ordinary identical config still is.
Untracked in #43, Refs #42.
Law: a zz override older than the file it overrides is a time bomb
Stow order means zz-$HOST wins, and when both packages carry the same path, sync relinks
it silently — the script's own comment: "managed file changing owners between packages:
silent relink, nothing to quarantine". dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl
held focus ring width 2 / active-color "#e01b24" from 7b7dbf5 (2026-10-17) while the
base package absorbed b3d3df4 (2026-10-18): width 4, "#ff8c00". Live was linked to
base, the newer intent, so --check printed RELINK .config/niri/cfg/layout.kdl on every
run and one unattended deploy.sh would have reverted the amber focus ring with no
quarantine and no warning. The duplicate is deleted in #43.
Rule: a zz file exists only to differ. When base absorbs the change, delete the zz copy
in the same commit — and never read RELINK lines from --check as cosmetic noise; they
are the sound of a stale override waiting for someone to run the tool for real.