docs: add technical writing guide
This commit is contained in:
208
AGENTS.md
208
AGENTS.md
@@ -1,110 +1,118 @@
|
||||
# dotfiles-nixos
|
||||
|
||||
One flake that builds every machine the user owns.
|
||||
The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overlays) lives in `~/Documents/ai-artifacts/projects/dotfiles/003-dotfiles-context.md`.
|
||||
One flake builds every machine I own.
|
||||
|
||||
## Conventions
|
||||
## Repository Conventions
|
||||
|
||||
- Comments posted to Gitea (pull requests, issues, reviews) go out under the operator's account, so sign every one to make clear the author is the agent, not the operator.
|
||||
End the comment with a `— Claude` sign-off.
|
||||
(A dedicated bot account may replace this later.
|
||||
Until then, the sign-off is the only marker.)
|
||||
- Commit messages follow Conventional Commits, specified in `docs/conventional-commits.md`.
|
||||
Scope is the module or host the change belongs to (`fish`, `nvim`, `neogaia`), omitted for repo-wide changes.
|
||||
Keep messages free of Gitea-specific references: this repository is mirrored to GitHub, where issue and pull-request numbers resolve to unrelated things.
|
||||
- When a graphical application is added, give it a `window-rewrite` icon mapping in `modules/desktop/waybar.nix`.
|
||||
Without one its windows fall back to the generic default glyph on the workspace indicator instead of showing a recognisable per-application icon.
|
||||
Match on the window class, which `hyprctl clients -j | jq -r '.[].class' | sort -u` lists for the running session.
|
||||
- Follow `docs/technical-writing.md` for comments, user-facing descriptions, commit prose, pull-request prose, and Markdown documentation.
|
||||
- Comments posted to the forge under the operator's account must end with `— Pi`.
|
||||
The sign-off identifies the agent as the author instead of the operator.
|
||||
- Commit messages follow Conventional Commits, defined in `docs/conventional-commits.md`.
|
||||
Use a scope for the changed module or host, such as `fish`, `nvim`, or `neogaia`.
|
||||
- Keep commit messages about the Git history, not the forge workflow.
|
||||
Do not reference issue or pull-request numbers unless the commit itself requires a stable external reference.
|
||||
- Use `gitea-axi` for Gitea issues, pull requests, labels, reviews, comments, and milestones.
|
||||
It resolves this repository from the `origin` remote inside the checkout.
|
||||
|
||||
## Dotfiles Conventions
|
||||
|
||||
### Desktop
|
||||
|
||||
- When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/waybar.nix`.
|
||||
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
|
||||
Match the mapping on the window class.
|
||||
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
|
||||
- Set Hyprland session environment variables in `modules/desktop/hyprland/hyprland.nix` with `env = KEY,VALUE`.
|
||||
`home.sessionVariables` do **not** reach the Hyprland session because UWSM does not source `hm-session-vars.sh`.
|
||||
Source cursor values from `config.stylix.cursor`.
|
||||
Set `XCURSOR_*` and `HYPRCURSOR_*` to the same theme for Bibata.
|
||||
- Configure the Steam desktop client as an XWayland application.
|
||||
SDL Wayland selectors do not make the visible client native Wayland.
|
||||
Keep fractional scaling sharp with Hyprland's `xwayland.force_zero_scaling` and Steam's `STEAM_FORCE_DESKTOPUI_SCALING`.
|
||||
|
||||
### Firefox
|
||||
|
||||
- Manage Firefox search engines through `programs.firefox.search` with `force = true`.
|
||||
Hide built-in engines with `<engine>.metaData.hidden = true` because omission does not remove them.
|
||||
Use current engine ids, such as `default = "ddg"` instead of `"DuckDuckGo"`.
|
||||
Decode the built `search.json.mozlz4` with `mozlz4a -d <search.json.mozlz4>` to check the result.
|
||||
- Avoid Home Manager Firefox extension settings unless the whole profile can use the legacy extension-storage backend.
|
||||
Any non-empty `profiles.<name>.extensions.settings.<id>.settings` sets `extensions.webextensions.ExtensionStorageIDB.enabled = false` for the whole profile.
|
||||
|
||||
### Neovim
|
||||
|
||||
- Nixvim's flake input follows the root nixpkgs source, but its Home Manager module does not reuse the host's `pkgs` instance.
|
||||
Keep `programs.nixvim.nixpkgs.useGlobalPackages = true`.
|
||||
That setting makes Nixvim use the shared package set without warning that its source default changed.
|
||||
|
||||
### AI Tooling
|
||||
|
||||
- Use produced literals for Herdr key names that include shifted punctuation.
|
||||
For example, use `prefix+<` instead of `prefix+shift+comma`.
|
||||
- Treat `~/.pi/agent/skills` as generated output from Home Manager.
|
||||
Do not edit skills in that directory.
|
||||
A new file created there stays outside the repo.
|
||||
Shared global skills come from the `skills` flake through `modules/agents/skills.nix`.
|
||||
Update the `skills` flake input, then rebuild.
|
||||
- Keep generated `.agents/skills/.gitignore` files from ignoring symlinked skills.
|
||||
Pi skill discovery honors `.gitignore`, `.ignore`, and `.fdignore` inside scanned skill directories.
|
||||
An ignored symlinked skill does not load even when `.agents/skills/<name>/SKILL.md` exists.
|
||||
- Write directly to flake-managed Pi extension, prompt, and skill directories only for throwaway development or local experiments.
|
||||
Home Manager activation can overwrite or hide unmanaged files in those directories.
|
||||
Promote finished work into the dotfiles module before treating it as deployed.
|
||||
|
||||
## Gotchas
|
||||
|
||||
### Architecture
|
||||
|
||||
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
|
||||
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
|
||||
- `sops.secrets.<name>.owner` must name a user known on the decrypting host.
|
||||
For guest-only service users, do not set the owner to the guest uid unless the host also declares that user.
|
||||
Use a host-visible owner plus a shared group mode when an identity-mapped guest service can read through its group.
|
||||
- Actual Budget's NixOS service runs `actual-server` with `WorkingDirectory = services.actual.settings.dataDir`, and the `actual-server` CLI wrapper sets `ACTUAL_CONFIG_PATH=./config.json` when that current directory contains `config.json`, overriding the NixOS module's generated config path before the server loads configuration.
|
||||
When migrating old community-script state to the normal upstream `/data` shape, move or rewrite a copied `/data/config.json` so it cannot shadow the declarative module settings.
|
||||
- Subagent completion delivery is non-blocking through immediate spawn, milestone notifications, retained terminal entries, and `subagent_list` or `subagent_result` retrieval.
|
||||
`subagent_wait` intentionally blocks the parent tool call until its condition or timeout, so do not use it merely to keep background work alive during an interactive workflow.
|
||||
- Nixvim's flake input following the root nixpkgs source does not make its Home Manager module reuse the host's `pkgs` instance.
|
||||
Keep `programs.nixvim.nixpkgs.useGlobalPackages = true` so Nixvim uses the shared package set without warning that its source default was affected.
|
||||
- This host has no `python` or `python3` command on its ordinary `PATH`.
|
||||
For ad hoc Python, use Nix explicitly, such as `nix shell nixpkgs#python3 -c python3 <script>`.
|
||||
- ADR bodies are immutable records of decisions as they were made, while frontmatter is mutable.
|
||||
When a decision changes or its premise proves wrong, preserve the original body, update its status, and add a new ADR that supersedes it.
|
||||
Filename migrations preserve references in immutable bodies through frontmatter aliases rather than rewriting those bodies.
|
||||
- This repo pins no Nix formatter, and its committed `.nix` files are not clean under current `nixfmt-rfc-style`.
|
||||
Running `nixfmt` across a file reflows untouched code (for example `lib.nix`'s `deriveMac` list and multi-line assertion messages) and injects churn unrelated to the change.
|
||||
Format only the lines being written or changed, matching the surrounding style by hand.
|
||||
- This repo is developed on `neogaia`, which now runs the NixOS it builds.
|
||||
Flakes and the chaotic substituter come from this flake's own `nix.settings`, so no `NIX_CONFIG` export or per-command `--extra-experimental-features` is needed, and building a toplevel with `boot.kernelPackages = linuxPackages_cachyos` fetches the kernel from `nyx-cache` rather than compiling it.
|
||||
Both were true only while the machine still ran CachyOS against a distro Nix daemon.
|
||||
- Git identity is declared in the flake by `modules/git.nix`, which writes `alexion <contact@alexion.dev>` — the identity all history uses — on any host enabling `modules.git`.
|
||||
Every new host has to enable it, so that a host reads as a full checklist of what it carries.
|
||||
It is deployed on `neogaia` and verified: a commit in a repository outside this checkout is authored `alexion <contact@alexion.dev>` with no override.
|
||||
Verify it that way rather than from this checkout, whose `.git/config` carries the same identity and would mask a broken module.
|
||||
`~/.gitconfig` (a second global file that outranks the flake-managed `~/.config/git/config`) currently holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, but it is undeclared and will not survive a reimage.
|
||||
- The primary build/verify seam is `nix flake check`, which builds project-level checks plus `checks.x86_64-linux.<host>` system toplevels.
|
||||
Cheap targeted host checks use `nix eval .#nixosConfigurations.<host>.config...`.
|
||||
- chaotic-nyx must **not** follow our `nixpkgs`, and its packages are built against chaotic's own pinned nixpkgs (its overlay defaults to `onTopOf = "flake-nixpkgs"`, the cache-friendly path).
|
||||
That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source.
|
||||
The tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays.
|
||||
- The remote is self-hosted Gitea (`git.alexion.dev`), and the forge CLI is `gitea-axi` rather than `tea`.
|
||||
`gitea-axi` resolves the repository from the `origin` remote and discovers credentials from a `tea` login whose host matches the remote, so both are implicit inside a checkout.
|
||||
It is installed on `neogaia` by `modules.agents.tools.gitea-axi`, and verified: `gitea-axi` run from this checkout renders the `alexion/dotfiles` dashboard authenticated, so the claude-code `SessionStart` hook that runs it now resolves to a real binary rather than a missing one.
|
||||
The package wraps the binary so `git` and `tea` are reachable without being on `PATH`, while still preferring the operator's own where present.
|
||||
Credentials: `~/.config/tea/config.yml` holds a token-bearing login named `alexion`, which `gitea-axi` uses and which also opens pull requests directly with `nix run nixpkgs#tea -- pr create --login alexion --repo alexion/dotfiles --base main --head <branch> ...`.
|
||||
The `--repo` flag is required on that path, since `tea` resolves `origin` only for a login whose SSH host matches.
|
||||
The same token reads PR discussion, which `tea` itself does poorly: `tea pr <n> --comments` prints only the body, and `-f comments` returns no comments field at all.
|
||||
Use the API instead, taking the token from `.logins[] | select(.name=="alexion") | .token`.
|
||||
Review comments are **not** at `/issues/<n>/comments` — that endpoint holds only top-level discussion and is usually empty.
|
||||
Inline comments need two calls: `/pulls/<n>/reviews` for the review ids, then `/pulls/<n>/reviews/<id>/comments` for the bodies, whose `path` and `diff_hunk` fields say what each one is attached to.
|
||||
A review row with an empty `body` is the normal shape when the operator left only inline comments.
|
||||
- `~/.claude/skills` and `~/.pi/agent/skills` are home-manager-generated (`recursive = true`), so editing a skill in place fails and a new file created there silently escapes the repo.
|
||||
Shared global skills come from the `skills` flake through `modules/agents/skills.nix`, applied by a rebuild.
|
||||
Claude-specific legacy skills, when kept, live under `modules/agents/claude-code/skills/<name>/`.
|
||||
- Pi skill discovery honors `.gitignore`, `.ignore`, and `.fdignore` inside scanned skill directories.
|
||||
A generated `.agents/skills/.gitignore` entry that ignores a symlinked skill also prevents Pi from loading that skill, even when `.agents/skills/<name>/SKILL.md` exists and the symlink target is valid.
|
||||
- nixpkgs `vimPlugins.nord-nvim` is `shaunsingh/nord.nvim` (no `require("nord").setup()`).
|
||||
The config wants `gbprod/nord.nvim`, which is packaged as `vimPlugins.gbprod-nord`.
|
||||
- `nixos-generate-config --show-hardware-config` needs root on this machine even just to print: unprivileged it dies at `Failed to retrieve subvolume info for /`, because the root filesystem is btrfs.
|
||||
- This repo's claude-code module sets sudo's credential cache to per-user (`timestamp_type=global`, 60-minute window), so an authentication made in one real terminal counts for the agent's commands.
|
||||
A `PreToolUse` hook refuses privileged commands while the cache is cold, so a cold cache announces itself instead of stalling.
|
||||
A privileged-command failure *without* that message is the sandbox, not the cache.
|
||||
- Host GPUs: `neogaia` is Intel, `zeus` (the desktop) is **AMD**, and `raichu` (a headless server) is the only Nvidia machine.
|
||||
The corrected fact also lives in artifact `006-dotfiles-hyprland-compositor-adr.md`.
|
||||
- This repo's `programs.firefox` `search` (with `force = true`) writes `search.json.mozlz4`.
|
||||
Omission alone does not prune a built-in engine, since Firefox reconciles its app-provided engines back in, so remove one by listing it with `<engine>.metaData.hidden = true`.
|
||||
Engines are referenced by their current id, so the default is `default = "ddg"`, not `"DuckDuckGo"`.
|
||||
Decode the built file with `mozlz4a -d <search.json.mozlz4>` to check the result.
|
||||
- Any non-empty Home Manager Firefox `profiles.<name>.extensions.settings.<id>.settings` causes Home Manager to set `extensions.webextensions.ExtensionStorageIDB.enabled = false` globally for that profile.
|
||||
This repo's Stylix Firefox `colorTheme` settings trigger it, so every extension in the profile uses the legacy extension-storage backend regardless of how it is installed.
|
||||
- `home.sessionVariables` do **not** reach the Hyprland session, since UWSM does not source `hm-session-vars.sh`.
|
||||
The cursor is therefore set through Hyprland's own `env = KEY,VALUE` in `modules/desktop/hyprland/hyprland.nix`, sourced from `config.stylix.cursor`.
|
||||
Bibata ships XCursor format only (no `hyprcursor/` dir), rendered through Hyprland's XCursor fallback, so `XCURSOR_*` and `HYPRCURSOR_*` naming the same theme are both safe.
|
||||
- `neogaia` is a wifi laptop with a btrfs root and no ZFS pools, so it cannot honestly carry `modules.network`, `modules.zfs`, or a networked/pool-mounted guest.
|
||||
Enabling networkd takes over its DNS, its CachyOS `zfs-kernel` build is marked broken, and it has no bridge or pool to attach to.
|
||||
Verify these against it ad hoc through `nixosConfigurations.neogaia.extendModules` (forcing a ZFS-capable `boot.kernelPackages` for the zfs case) plus `nix eval` of the derived values, never by committing the enablement.
|
||||
Committed networked and pool-mounted guests belong on a wired storage host such as `pikachu`, not on `neogaia`.
|
||||
- Herdr key names for shifted punctuation are not interchangeable with the physical base key plus `shift`.
|
||||
The tab rename binding must use the produced literal, such as `prefix+<`, rather than `prefix+shift+comma`.
|
||||
- Flake-managed Pi extension, prompt, and skill directories may still be written directly for throwaway development or local experiments.
|
||||
The risk is that a later Home Manager activation can overwrite or hide those unmanaged files, so finished work must be promoted into the dotfiles module before it counts as deployed.
|
||||
- Pi's tool discovery checks `~/.pi/agent/bin` before `PATH`, and downloaded generic Linux binaries there can be unusable on NixOS with the stub-ld error.
|
||||
This flake patches Pi to validate local tool binaries before selecting them, so it falls back to usable `fd`/`rg` from `PATH` instead.
|
||||
Stale unpatched launchers are the remaining failure mode for broken `@` autocomplete.
|
||||
- Nix flake evaluation ignores untracked files in this checkout.
|
||||
Keep a new auto-loaded module staged or committed until it is removed, otherwise `nix flake check` and `nixos-rebuild --flake` evaluate without it and report its options as missing.
|
||||
- The current Steam desktop client is an XWayland application.
|
||||
Its CEF windows do not support Ozone and Steam composites them into an SDL surface with X11 extensions, so SDL Wayland selectors do not make the visible client native Wayland.
|
||||
Keep fractional scaling sharp with Hyprland's `xwayland.force_zero_scaling` and Steam's own `STEAM_FORCE_DESKTOPUI_SCALING` instead.
|
||||
- AdGuard's LAN wildcard rewrite for `*.alexion.dev` points names at the reverse-proxy guest, including non-HTTP ports.
|
||||
Keep `git.alexion.dev:2022` working by proxying that TCP stream through the reverse-proxy guest to Gitea.
|
||||
- `self.lib.registry.collect { hosts = self.nixosConfigurations; }` reads only top-level host `config.registry` declarations.
|
||||
Registry declarations inside guest interiors live under `config.containers.<guest>.config.registry`.
|
||||
The collected registry does not include those guest-interior declarations.
|
||||
Publish guest values from host-side code, or extend the collector to recurse into guests.
|
||||
See http://git.alexion.dev/alexion/dotfiles/issues/65.
|
||||
- A NixOS module function argument default such as `registry ? config.registry` does not safely handle an unprovided module argument.
|
||||
The module system still queries `_module.args.registry`.
|
||||
Evaluation fails when that argument is absent.
|
||||
For an optional special-argument fallback, bind module arguments through `@args`.
|
||||
Test `args ? <name>` inside the module body.
|
||||
|
||||
### Build Inputs
|
||||
|
||||
- `chaotic-nyx` must **not** follow this flake's `nixpkgs`.
|
||||
Chaotic builds its packages against its own pinned `nixpkgs`.
|
||||
Its overlay defaults to `onTopOf = "flake-nixpkgs"`, the cache-friendly path.
|
||||
That setup lets `nyx-cache.chaotic.cx` serve the CachyOS kernel instead of building it from source.
|
||||
Chaotic packages do not see this flake's `unstable` or `stable` overlays.
|
||||
|
||||
### Deployment
|
||||
|
||||
- Remote deploy helpers that run target-side `sudo` over SSH need a TTY.
|
||||
Use `ssh -t` unless the target command is guaranteed not to prompt for sudo credentials.
|
||||
- `self.lib.registry.collect { hosts = self.nixosConfigurations; }` reads top-level host `config.registry` declarations only.
|
||||
Registry declarations inside guest interiors live under `config.containers.<guest>.config.registry` and do not enter the collected fleet registry unless host-side code also publishes them or the collector is explicitly extended to recurse into guests.
|
||||
- A NixOS module function argument default such as `registry ? config.registry` is not a safe fallback for an unprovided module argument.
|
||||
The module system still queries `_module.args.registry` and fails when it is absent.
|
||||
When an optional special-argument fallback is intentionally needed, bind the module arguments through `@args` and test `args ? <name>` inside the module body.
|
||||
Use `ssh -t` unless the target command cannot prompt for sudo credentials.
|
||||
|
||||
### Host Environment
|
||||
|
||||
- This host has no `python` or `python3` command on its ordinary `PATH`.
|
||||
For one-off Python commands, use Nix explicitly, such as `nix shell nixpkgs#python3 -c python3 <script>`.
|
||||
- `nixos-generate-config --show-hardware-config` needs root on this machine, even when it only prints.
|
||||
Without root, it fails with `Failed to retrieve subvolume info for /`.
|
||||
The root filesystem uses btrfs.
|
||||
- Nix flake evaluation ignores untracked files in this checkout.
|
||||
Stage or commit new auto-loaded modules before evaluating the flake.
|
||||
Otherwise, `nix flake check` and `nixos-rebuild --flake` cannot see those modules.
|
||||
They can then report missing options that exist only in the working tree.
|
||||
|
||||
### Secrets
|
||||
|
||||
- `sops.secrets.<name>.owner` must name a user known on the decrypting host.
|
||||
Do not set the owner to a guest-only service user unless the host also declares that user.
|
||||
Use a host-visible owner plus a shared group mode when the guest service can read the decrypted secret file through its group.
|
||||
|
||||
### Service Migrations
|
||||
|
||||
- Actual Budget's NixOS service runs `actual-server` with `WorkingDirectory = services.actual.settings.dataDir`.
|
||||
The `actual-server` CLI wrapper sets `ACTUAL_CONFIG_PATH=./config.json` when the working directory contains `config.json`.
|
||||
That environment variable overrides the config path generated by the NixOS module.
|
||||
During migration from old community-script state, move or rewrite a copied `/data/config.json`.
|
||||
Otherwise, the copied file shadows the declarative module settings.
|
||||
|
||||
Reference in New Issue
Block a user