Objective

Evaluate adopting Gabriel Volpe’s “Private Nix flake” pattern to add a host axis (personal vs. enterprise) on top of the current architecture-only split in Nix - Home Manager, without refactoring this repo or cluttering it with inline override hooks.

Context

Current dotfiles at ~/.config only vary by system (aarch64-darwin, x86_64-darwin, aarch64-linux, x86_64-linux) via forAllSystems in nix/flake.nix. Host-level customization (personal vs. work machine on the same arch) is not supported. Two ad-hoc override hooks exist today:

Read as of 2026-04-24. Both statements have since been overtaken: local.* capability flags plus local.profile cover the role axis (2026-07-04/05), and hosts/local.nix covers per-host identity (2026-07-19). The “not supported” clause was also never quite right — the corp machines were already customizing downstream through these very hooks.

  • nix/home-manager/modules/packages.nix:6-12 — inline overrideAdd / overrideRemove arrays relying on git rerere to absorb rebase conflicts.
  • nix/home-manager/modules/coding-agents/claude/claude.nix:46-60 — sidecar claude-overrides.nix loaded via builtins.pathExists.

Research conducted 2026-04-24 found:

  • Home-manager has no first-class host abstraction (HM FAQ just says “one top-level file per user-machine combo”).
  • No native “override file” convention under flakes; legacy ~/.config/nixpkgs/overlays/ is inert under pure eval.
  • Community canon splits into four patterns: hosts/<hostname>/ directories (Misterio77, EmergentMind), option-based feature flags (options.my.*), flake-parts per-host modules (srid, Lite-system), and composable profile stacks.

Approach

Prefer downstream-as-input over a host-axis refactor of this repo:

  • Public repo (this one) stays minimal and unchanged.
  • A separate private flake takes this as a flake input, layers its own hosts/work.nix + overlay exposing mkHomeConfigurations per Volpe’s writeup.
  • “Rebase on upstream” collapses to nix flake update <input> on the private flake.
  • Secrets stay out of the public repo entirely; use sops-nix or agenix in the private flake.

Alternative (rejected for current scope): refactor this repo to Misterio77/EmergentMind-style host axis with hosts/, specialArgs, options.my.*. Touches every module; high churn; only worthwhile if multiple hosts need to co-exist in one repo.

Next Actions

  • Decide whether the enterprise use case is concrete enough to warrant creating a private flake yet, or whether the existing overrideAdd / claude-overrides.nix hooks are sufficient for now. Answered 2026-07-19, and the question was mis-framed. The corp machines have been running these dotfiles the whole time, via a rebased downstream branch. The hooks were never a stopgap awaiting a decision; they were the mechanism in production. See the 2026-07-19 correction below.
  • If proceeding: scaffold a private flake with this repo as input, one host module, and a test overlay that swaps one small surface (e.g. isTrusted, git identity, or a coding-agent API base URL).
  • Validate that the override-priority mechanics (lib.mkForce, disabledModules) actually compose cleanly against this repo’s modules without per-module changes.
  • Consider whether the existing claude-overrides.nix sidecar pattern should be generalized or retired once the private-flake approach is in place.
  • Smaller stepping-stone option: refactor one module (e.g. git.nix or claude.nix) to declare options.dotfiles.<feature>.* with mkEnableOption / mkPackageOption, gated via lib.mkIf. Validates whether a Misterio77-style feature-flag layer composes cleanly with the rest of the tree before committing to the full private-flake refactor. Lower-risk than scaffolding a downstream consumer; reversible if it doesn’t pay off.

Resources

  • Gabriel Volpe — “Private Nix flake” — primary reference pattern.
  • Lorenz Bischof — “Manage secrets in NixOS with a private repository” — same pattern, secrets framing.
  • Michael Maclean — “NixOS with private flakes” — SSH agent forwarding setup.
  • Misterio77/nix-config — canonical hosts/<hostname>/ reference (rejected alternative layout).
  • EmergentMind/nix-confighosts/common/{core,optional} layered imports.
  • srid/nixos-config — flake-parts approach.
  • NixOS Discourse — Hostname-agnostic nix-darwin configuration — endorses named configurations and git-branch-per-machine.
  • Jade Lovelace — “Flakes aren’t real” — dissenting view worth reading before committing to flake-heavy layering.
  • mightyiam/dendritic and vic/den — recently formalized “every file is a flake-parts module” pattern. Filtered out as overkill for the 2-host scale here. Revisit trigger revised 2026-07-19: guard-test coverage, not host count. The original trigger (~5 machines, or shared community modules becoming attractive) turned out to be the weaker signal. Dendritic’s real advantage here is that it collapses axis 1 entirely: with no curated import list, there is no composition-based mechanism that can mask a dead annotation-based flag — the exact bug found 2026-07-19, where local.desktopApps.enable gated nothing for two weeks while looking correct, because import omission on athena was already producing the right outcome. The cost of collapsing axis 1 is that a missed enable guard reaches the x86_64-darwin build. That cost is falling: Intel breakage measured at zero errors across the full closure (twice), and tests/capability-flags.nix now detects missed guards directly. The honest trigger is when that test covers the heavy Intel-breaking modules (litellm/arrow-cpp, open-webui, signoz) rather than just desktopApps / python.tools — at that point the import boundary is protecting against a failure the suite already catches, and axis 1 is pure cost.

Trigger met the same day (2026-07-19, commit 8ae96b1). Contracts now cover all three heavy modules across four surfaces (home.file targets, activation names, launchd agents, package names), verified on all three systems. Extending them surfaced that programs.litellm.enable did not exist at all — the module’s config was unconditional, so only import omission kept the arrow-cpp puller off athena. That is the refactor the 2026-07-04 scoping section above called “the one required module refactor,” still undone 15 days later and invisible to every check.

The trigger being met does not settle the question, and I am not treating it as settled. The residual argument for axis 1 is narrower than the original: the contract test asserts artifacts disappear, not that the closure is free of Intel-broken derivations. A module could pass its contract and still pull a broken dependency through a path no contract names. That is a real gap, but it is a much smaller one than “a missed guard is silent.” Deciding whether it justifies a hand-maintained import list is a judgement call worth making deliberately rather than by default. See Dendritic Pattern Borrows AOP Vocabulary But Realizes SPL Composition and Nix Module System Realizes Multi-Dimensional Separation of Concerns.

Notes

Status is paused because the enterprise host isn’t yet a concrete requirement. Unblocked when: (1) a specific work machine materializes, or (2) the current overrideAdd / claude-overrides.nix hooks start causing rebase pain.

Superseded 2026-07-19. Both unblock conditions were written as future events, and both had already happened. The corp Linux workstations exist, and they consume this repo through a rebased downstream branch rather than as a flake input. The homeConfigurations.aarch64-linux / .x86_64-linux entries in nix/flake.nix are those hosts, not portability fixtures.

That reframes the private-flake question rather than answering it. The pattern was reserved here for two things: secrets, and trusted = false. Neither is solved today. What the correction removes is only the precondition — there is no machine to wait for, and the honest comparison is now downstream-branch-plus-sidecar (in production, working) against downstream-as-input (unbuilt). The branch approach has a real advantage the original note did not anticipate: a file that exists only downstream, like hosts/local.nix, never conflicts on rebase, because upstream never touches the path. That is strictly cheaper than the tracked overrideAdd arrays it sits beside, which re-resolve a conflict every rebase via git rerere.

The live cost is therefore narrower than “we need a private flake”: corp hostnames and any corp-specific secrets must not reach the public repo. The sidecar already covers identity. Secrets are the remaining gap, and secrets alone may not justify a downstream flake when sops-nix or agenix could be pointed at a downstream-only path in the same way.

Consequence to keep in view: any change to the desktop profile ships to a corp box on its next rebase. This repo has more than two consumers, and one of them is not personally owned.

Non-obvious finding from research: disabledModules matches by path, so any upstream tree restructure would silently break a downstream consumer relying on it. That argues for keeping module paths stable in this repo once downstream consumers exist, or for preferring lib.mkForce over disabledModules in the private flake.

Academic-frame addition (2026-04-27): the multi-host variability problem is a textbook fit for Tarr et al.’s Multi-Dimensional Separation of Concerns (ICSE 1999), realized over Dolstra & Hemel’s purely-functional configuration paradigm (HotOS 2007). The Software Product Lines / Feature-Oriented Programming literature (Ferreira et al. 2014; Gaia et al. 2014) maps more cleanly to what the Nix module system enables than the “aspect-oriented” framing the dendritic community uses. This doesn’t change the architectural recommendation — Misterio77-style feature flags remain the lowest-friction path — but provides defensible grounding if the choice ever needs to be written up. See Academic Foundations of Nix Configuration Patterns.

Module-system primitives worth remembering for whichever path is taken: mkEnableOption "<feature>" (boolean toggle shorthand), mkPackageOption pkgs "<name>" {} (typed package selection), mkDefault (priority 1000) / mkForce (priority 50) for layered overrides, extraSpecialArgs (vs _module.args) when imports need to depend on host or profile.

Addendum 2026-07-04 — Capability-flag model + athena as concrete trigger

Revisited while scoping a Home Manager rollout to athena (Intel Mac home server, x86_64-darwin). This surfaces a second variability axis the original note never scoped: not personal-vs-enterprise on the same arch, but role + arch (desktop vs. headless server; aarch64- vs. x86_64-darwin). Status flipped paused → active: unblock trigger (2) — “override hooks start causing pain” — is now superseded by a concrete host need.

Chosen mechanism — capabilities-as-options. Each module declares options.dotfiles.<feature>.enable = mkEnableOption ... and wraps its body in lib.mkIf; a per-host module (hosts/<name>.nix) is just the opt-in list; profiles are named bundles (desktop, server, headless, work). This promotes the note’s own “smaller stepping-stone” feature-flag idea to the primary recommendation for the role/arch axis. The private-flake / downstream-as-input pattern stays reserved for the enterprise host only (secrets + trusted = false, consuming this repo as an input).

Machine → profile mapping (capability matrix image generated 2026-07-04 at ~/Pictures/generated/hm-capability-matrix.png):

  • atlas — aarch64-darwin, desktop: everything (desktop-gui, ai-stack on).
  • athena — x86_64-darwin, server: shell / neovim / git / dev-tools / media-server / obsidian / remote-mcp; desktop-gui + ai-stack off.
  • oracle (OCI) — aarch64-linux NixOS, headless: shell / git / remote-mcp only (HM-as-NixOS-module, not a standalone homeConfiguration).
  • enterprisework via private flake: dev core + secrets; personal AI / obsidian / mcp off.

Prerequisite the matrix hides: flake.nix:118 intentionally excludes x86_64-darwin (arrow-cpp marked broken, pulled transitively by the LiteLLM stack). athena needs the arch admitted to systems and the homeConfigurations genAttrs list first; the capability flags are what make that admission safe (heavy modules never enter the graph).

Scoping findings (2026-07-04, the real work):

  • Most heavy modules already self-gate on enable (signoz.nix, open-webui.nix, tailscale-serve.nix, obsidian.nix) and are off unless home.nix turns them on → not athena blockers.
  • modules/litellm/default.nix has NO enable option and NO mkIf gate — unconditionally active on Darwin, and the direct arrow-cpp puller. Adding programs.litellm.enable and wrapping its config in mkIf is the one required module refactor. Caveat: the options block must stay ungated because jupyter.nix and sillytavern.nix read config.programs.litellm.port / .defaultClaudeModel; only the config (packages, launchd agent, files) gets mkIf.
  • modules/python.nix (uv2nix venvs incl. jupyterlab) and shell.nix:106 (ghostty-bin, an aarch64-darwin prebuilt) also need gating / arch-widening for x86_64-darwin.
  • Unbounded risk = Intel-arch whack-a-mole: the full set of x86_64-darwin-broken transitive deps is only discoverable by running hm build on athena; each failure reveals the next path to gate or overrideRemove (the hook at packages.nix:6-12 already supports per-host removal). Static reading cannot enumerate it. Recommended de-risking order: prototype athena with a curated-import entry point (bypass the home.nix auto-discovery, list only the safe core), get it building, then convert the survivors to dotfiles.<feature> flags.

Implementation status (2026-07-04) — step 1 landed (staged, not yet committed):

  • Extracted nix/home-manager/base.nix (shared host-agnostic identity/XDG/Nix-daemon settings), imported by both home.nix (desktop) and the new curated home-server.nix (server profile).

  • Admitted x86_64-darwin to the flake systems (server profile only); refactored homeConfigurations into a mkHome helper + homeConfigurations."x86_64-darwin" -> home-server.nix; kept checks desktop-only.

  • shell.nix: ghostty gated off on x86_64-darwin (no prebuilt, headless); uvLib made optional. Footgun found: a formal-level uvLib ? null default does not work — the HM module system eagerly passes every declared function arg, shadowing the default. Fix: base.nix sets _module.args.uvLib = lib.mkDefault null, which python.nix overrides on the desktop.

  • Verified on atlas: activationPackage.drvPath byte-identical before/after (pure refactor, gjlka2k6…); athena homeConfigurations.x86_64-darwin evaluates (9zc85ann…) so no broken-marked packages in the curated closure; just check passes. Capability matrix image at ~/Pictures/generated/hm-capability-matrix.png. See 2026-07-04 Home Manager athena Server Profile. Update 2026-07-05 — athena is LIVE. Home Manager is now declaratively managing athena (generation 1 current). Full end-to-end bring-up done; see 2026-07-04 Home Manager athena Server Profile.

  • Headline finding: the entire profile — including every desktop GUI app (Zotero, Obsidian, browsers, tectonic) — builds on x86_64-darwin with zero Intel breakage. The feared package whack-a-mole never materialised; 0 errors across the full closure. The only cost is time: cache.nixos.org has thin x86_64-darwin coverage (the sunset), so much of nixpkgs builds from source. The extra caches (nix-community/garnix) don’t mirror it either, so trusted-users doesn’t rescue build speed.

  • Bring-up path: Determinate installer has no x86_64-darwin binary (Intel sunset) → used the upstream nixos.org/nix/install --daemon; adopted ~/.config as the repo in place (gh/secrets gitignored, no clobber); extra-trusted-users = achhina in /etc/nix/nix.conf; first activation via nix run home-manager/master -- switch --flake ~/.config/nix#x86_64-darwin -b backup.

  • packages.nix trimmed (commit 4c6f3cb): local.desktopApps.enable (default true) gates the desktop extras (guiApps + documentTools + per-OS GUI bundles); home-server.nix sets it false → athena 114→87 packages, no Zotero/Obsidian/GUI. First real dotfiles-style capability flag (lives under local.* for now).

  • Two fresh-bootstrap gotchas found + fixed: (1) non-interactive SSH has no nix on PATH and flakes disabled → drive with the full /nix/var/nix/profiles/default/bin/nix path + NIX_CONFIG='extra-experimental-features = nix-command flakes' + exported XDG_*. (2) mkZshCompletionGen wrote to $XDG_DATA_HOME (runtime env) under set -u; unbound on a bare-shell bootstrap → aborted activation. Fixed by baking config.xdg.dataHome at store time (commit 183ed19). Never bit desktop hosts (a running HM session always exports XDG_*).

  • Commits (all now on main, see next section): 2aff736 (server profile), 4c6f3cb (packages split), 183ed19 (xdg fix).

Update 2026-07-05 — merged to main + second capability flag (devTools)

Follow-ups from the bring-up landed. All four commits are now on main (fast-forward, both feature branches deleted).

  • Merge: hm-athena-server-profile (3 commits) fast-forwarded into main and pushed.
  • Second capability flag — local.devTools (commit 4badaf5). Added a dev-toolchain master switch with independent languageServers / formatters / runtimes sub-toggles, following the releaseCooldown master+sub-toggle idiom already in machine.nix (each sub-toggle defaults to the master). packages.nix gates its three dev groups on these via optionals in original list position. home-server.nix sets local.devTools.enable = false → athena 87→60 packages (drops 11 LSPs + 8 formatters/linters + 8 runtimes/compilers). atlas drvPath byte-identical before/after (229c3i27…) so the desktop host does not rebuild — the optionals true xs == xs no-reorder property confirmed empirically via stash-and-diff.
  • Design decision — the curated import list stays curated. Deliberately did not migrate home-server.nix’s hand-picked imports to full home.nix auto-discovery + per-module disable flags. Doing so would require a correct enable guard on every Intel-broken heavy module (litellm/arrow-cpp, open-webui, signoz, jupyter, python…); one missed guard re-breaks the x86_64-darwin build. Codified the two independent axes in home-server.nix’s header comment: (1) which modules load = curated imports, the arch-safety boundary, stays hand-managed; (2) which packages loaded modules install = capability flags (desktopApps, devTools), where all content trimming happens. Capability flags currently live under local.* (not the aspirational dotfiles.* namespace) for consistency with the existing local.desktopApps.enable.
  • Deployed to athena (generation 2): hm switch over SSH applied the trim (87→60 packages live) + the xdg fix; expired generation 1 and GC’d 5.8 GiB (the reclaimed dev toolchain — rust crates, go, nodejs, LSPs).

Update 2026-07-05 (later) — third capability flag: local.python (uv on the server)

Found that uv was not behind any capability flag — it rode inside python.nix, gated purely by module import (desktop auto-discovery pulls it; the server’s curated list didn’t). Promoted it to a real capability so athena can have uv without the heavy data-science tooling.

  • local.python (machine.nix, commit e1efaef): enable (master, default true — uv/uvx CLI + managed CPython + uvLib + the local uv2nix tools) plus tools.enable (default = master — the heavy uv tool install set: jupyterlab/ipython/comfy-cli/pyzotero/scalene/memray, several pulling torch/Jupyter into ~/.local). Same master+sub-toggle idiom as devTools/releaseCooldown.
  • python.nix gating: programs.uv + installPythonVersions + _module.args.uvLib on python.enable; installUvTools on python.enable && tools.enable. uvLib is mkIf python.enable {...} so when disabled it falls back to base.nix’s mkDefault null and shell.nix’s hasUv gate skips the local tools correctly — no leaky flag.
  • home-server.nix imports ./modules/python.nix and sets local.python.tools.enable = false (enable stays true). athena now has uv 0.11.25, managed CPython 3.10–3.15, and the uv2nix-built claude-ops + gh-review-preview, but uv tool list is empty (no torch/jupyter/comfy).
  • The uv2nix venvs build cleanly on x86_64-darwin — the Intel whack-a-mole again failed to materialise. Also fixed a latent warning: generateCheckUpstreamIssuesCompletion shelled out to uv unconditionally and warned (exit 127) on the uv-less server; gated it on hasUv (commit 26428ab). With uv now present it runs cleanly anyway.
  • Verification held: atlas drvPath byte-identical at 229c3i27… across all of devTools + python (all flags default true → mkIf/optionals no-op). Deployed to athena (generation 3), expired generation 2, GC’d another 437.6 MiB.
  • All on main: 4badaf5 (devTools), 26428ab (uv completion gate), e1efaef (local.python). Three capability flags now real: desktopApps, devTools, python.
  • Named profile bundle landed (commit ecef30e): local.profile (enum desktop|server, default desktop). machine.nix maps profile == "server"{ desktopApps.enable, devTools.enable, python.tools.enable } = mkDefault false; home-server.nix collapses to the single local.profile = "server". mkDefault (1000) beats the option default (mkOptionDefault 1500) but yields to an explicit host override (100), so a server host can still flip one capability back on. Pure refactor — both drvPaths unchanged (atlas 229c3i27… via mkIf false; athena gqa9ngn8…, so the athena redeploy was a confirmed no-op, still generation 3). Adding a second server host (oracle) is now a one-line local.profile = "server". This realizes the note’s original “profiles = named bundles” recommendation.
  • Still open: extend to oracle (headless NixOS, HM-as-NixOS-module — the second profile = "server" consumer, currently the idling OCI box) and the enterprise private-flake host (still blocked on a concrete machine + secrets)stale, see the 2026-07-19 correction: the corp hosts exist and already run this repo.

The capability model is now complete for the personal hosts: three flags (desktopApps, devTools, python) + a local.profile bundle, all landed on main (2aff736 · 4c6f3cb · 183ed19 · 4badaf5 · 26428ab · e1efaef · ecef30e), atlas provably unchanged throughout, athena live on all of it.

Update 2026-07-19 — the model was never tested; desktopApps was inert

Review of the landed model (see 2026-07-19 Capability Flags Were Never Tested). The line above — “the capability model is now complete for the personal hosts” — was premature. Two of the three flags were partly decorative.

  • local.desktopApps.enable gated only a package list. Read at packages.nix:189 and nowhere else. tridactyl / zotero / sillytavern deployed unconditionally; aerospace / bettertouchtool gated on isDarwin alone. Its own description claimed it gated the desktop modules. Now guarded with lib.mkIf in all five.
  • local.python.tools.enable did not gate jupyter / ipython config. Only the activation-time uv tool install was gated, so the config files deployed on a host with none of the tools. Both modules now gate on it (not on desktopApps — this is data-science tooling, not GUI).
  • Root cause is structural, not sloppiness. checks builds only the desktop profile, where every flag is true, so a flag gating nothing was indistinguishable from a working one. The flags had no false-state coverage for two weeks.

Fix: nix/tests/capability-flags.nix. Re-evaluates the desktop config with each flag off via extendModules and asserts the artifacts disappear. Eval-only, so it does not drag athena’s x86_64-darwin closure into nix flake check (the documented reason home-server.nix is excluded from checks) — that constraint made the obvious “just add athena to checks” fix wrong.

Test-design footgun worth keeping. The first version keyed on home.file attribute names and passed while asserting nothing: xdg.configFile entries land under an absolute key while home.file entries stay relative, so the tridactyl contract matched nothing. Same absolute-key drift as the 2026-07-01 HM bump on settings.json. .target is stable across both; the attribute name is not. The test now also fails on any contract target absent from the baseline, so a vacuous contract cannot pass.

Also landed: assertions for machineName == "localhost" (a silent-wrong-value default that mislabels OTel resource attrs rather than failing) and python.tools.enable -> python.enable (a half-state that installs nothing quietly). Documented the whole model in docs/decisions/0021-capability-flags-host-profiles.md — the first in-repo record; the rationale previously lived only in this note and inline comments.

Two findings recorded but not acted on:

  • local.python.enable = false is unsupported on the desktop module set. claude.nix dereferences the uvLib module arg unconditionally, where shell.nix correctly guards on hasUv != null. The option description (“set false on profiles that ship no Python at all”) only holds for the curated server import list.
  • The axis-1 justification is weaker than it reads. The curated import list is justified by Intel breakage, but the bring-up found zero Intel errors across the full closure. arrow-cpp was real, so the decision stands, but the note above already recorded the whack-a-mole failing to materialise twice, and the header comment does not reflect that.

atlas drvPath unchanged (zvdqhbqmm3k9…), verified via a worktree at HEAD rather than stash-and-diff. just check + nix flake check clean.

Next Actions (2026-07-19)

  • Decide: guard uvLib in claude.nix, or narrow the local.python.enable description to match reality.
  • home.nix hardcodes local.machineName = "atlas" while serving three system doubles; the Linux desktop configs mislabel their telemetry. Done 2026-07-19 (commit fb61d08). home.nix is now a pure desktop profile with no machine identity; atlas moved to home-manager/hosts/atlas.nix. Flake outputs unchanged.
    • Correction: the Linux configs are real corporate hosts. A first pass named them portability-<system> on the reasoning that Hostname Naming Convention’s fleet table lists no Linux machine. That table is a naming policy whose scope clause excludes work-issued hardware; I read the exclusion as non-existence. The enterprise host this whole project note reserves the private-flake pattern for already exists and is already running these dotfiles via a rebased copy — the very workflow the overrideAdd/overrideRemove hooks in packages.nix were built for. Worth carrying forward: the “enterprise host blocked on a concrete machine” line elsewhere in this note is also stale.
    • Identity for those hosts comes from home-manager/hosts/local.nix: absent upstream, committed on the branch each machine tracks. Upstream never touches the path, so it survives a rebase without conflicting and needs no rerere, unlike the tracked override arrays. Falls back to unconfigured-<system> at mkDefault when absent — greppable, colliding with nothing, versus the old behaviour of silently claiming to be atlas. hosts/local.nix.example documents it.
    • The sidecar must not be gitignored. A flake’s source is the set of git-tracked files, so builtins.pathExists cannot see an ignored path. It still resolves under nix eval --impure, which is what makes this easy to miss: that is not the path hm switch takes. Confirmed both directions before committing.
    • mkHome now takes a list of entry modules, composing a host as [profile, host] rather than a host file that imports the profile. Not stylistic: the wrapper form evaluates the host before the profile it imports, reordering every list-valued option the profile contributes to. home.packages is a buildEnv input list, so the obvious shape rebuilt atlas for a byte-identical set of 101 packages. Caught only because drvPath moved when it should not have; the surface diff showed files and activation identical and packages a pure reordering. Nesting a module is not free.
    • Not done: home-server.nix still mixes profile and identity for athena, because its curated import list is the server profile. Splitting it is a larger change and would want a profiles/server.nix + hosts/athena.nix pair.
  • Consider moving local.telemetry.* into machine.nix so the local.* tree has a single owner (home-server.nix imports no telemetry module, so those options do not exist on athena at all).
  • Extend contract coverage to local.devTools.*, which has no false-state test yet.
  • Decide the secrets story for the corp hosts, now the only live argument left for the private-flake pattern. Compare downstream-only sops-nix/agenix paths (same shape as the hosts/local.nix sidecar, no new flake) against downstream-as-input. Identity is already solved; do not re-solve it.

Update 2026-07-20 — lib.uv refactor + athena pinned to 26.05 (both on main)

Two flake-architecture changes landed, from a discussion that started on where this module set sits relative to AOP / FOP / dendritic. See 2026-07-20 lib.uv Refactor and athena 26.05 Pin.

uvLib was a hidden transitive dependency; now lib.uv.mkProject (commit 7b40430). The uv2nix builder was injected via _module.args.uvLib: python.nix set it under mkIf local.python.enable, base.nix defaulted it to null, consumers guarded on hasUv = uvLib != null. A module’s value depended on whether python.nix was imported — an edge written nowhere. claude.nix dereferenced it unconditionally, safe only because the one host with python.enable = false also happened not to import claude.nix (the same masking pattern as the 2026-07-19 desktopApps bug). Moved the builder to nix/lib/uv.nix, exposed as lib.uv.mkProject by extending the module-arg lib at the flake boundary in mkHome (the EmergentMind/Misterio lib.extend convention; HM re-extends the passed lib with lib.hm and .extend composes, so modules see both). The real fix is decoupling the builder from the capability flag: building a local tool is a pure function of flake inputs, orthogonal to provisioning the Python stack, which local.python.enable had conflated. hasUv is gone; generateCheckUpstreamIssuesCompletion (a uv run tool needing the CLI at runtime) now gates on local.python.enable specifically. atlas + athena drvPath byte-identical, so neither rebuilds. This reverses the _module.args.uvLib decision recorded in ADR 0003.

athena (x86_64-darwin) pinned to nixpkgs-26.05-darwin (commit 20d7075). nixpkgs-unstable dropped x86_64-darwin (Intel Mac sunset), so the shared unstable nixpkgs now aborts instantiating that system and athena was frozen. Pinned only x86_64-darwin’s nixpkgs to the maintained 26.05-darwin branch (last release supporting the platform, tip current), selected per-system in perSystem and the base16 helper.

  • Not a matched HM pin. A first pass added home-manager release-26.05; eval failed on programs.fzf.changeDirWidget does not exist — these modules use HM-master-only options, so pinning HM back breaks the config rather than fixing it. HM stays on master; only nixpkgs moves, the minimal delta from athena’s prior working env.
  • Not a frozen unstable SHA. 26.05-darwin is maintained (tip dated today) and is what nixpkgs’ own deprecation warning recommends.
  • HM-master-vs-26.05 skew surfaced one assertion (HM defaults enableNushellIntegration on, asserts fzf >= 0.73; 26.05 ships 0.72). Nushell is unused, now explicitly off.
  • Eval-clean is not build-clean. x86_64-darwin evaluates end-to-end, but it cannot build on the aarch64 host, so build-time skew in the unstable-following inputs (stylix, the uv2nix/pyproject trio) built against 26.05 pkgs can only surface on athena’s own hm switch. That switch is the outstanding validation; expect a harmless home.enableNixpkgsReleaseCheck warning for the intentional HM/nixpkgs mismatch.

Update 2026-07-20 (later) — capability taxonomy: python.tools decomposed by purpose

Two more commits, from the same conversation turning to the capability model itself. See 2026-07-20 Capability Taxonomy - python.tools Decomposed by Purpose.

The smell: local.python.tools gated seven uv-installed tools (jupyter/ipython, comfy-cli, pyzotero, scalene/memray, claude-code-transcripts) whose only common trait was the uv tool install mechanism. That is a mechanism-grouping masquerading as a capability — purpose scattered across the install axis. Also audited how much axis 1 actually buys: of 42 top-level modules, the curated import list is load-bearing for only ~9 (copier/docker/media-stack/… that would activate if auto-discovered on the server); the other 14 desktop-only modules are redundantly gated by a capability flag or a default-off enable. That is the concrete cost of a future dendritic axis-1 collapse — those 9 need explicit guards first.

Commit d0ae738 — split by purpose, mechanism stays shared. local.dataScience.enable (jupyter/ipython + kernels), local.devTools.profilers.enable (scalene/memray, a 4th devTools subgroup), and python.tools shrunk to the residual. python.nix partitions the tool set into purpose groups; the activation installs the union of the enabled ones. Key-sorted iteration keeps all-on byte-identical — atlas + athena drvPaths unchanged.

Commit 8194f26 — dissolve the residual. claude-code-transcripts removed outright; comfy-cli and pyzotero became individual programs.comfy-cli.enable / programs.pyzotero.enable opt-in flags (declared in python.nix, default off, enabled in the desktop profile per an AskUserQuestion — desktop+corp keep them). local.python.tools deleted entirely (option + assertion + server-profile line). atlas drvPath changed by exactly the cc-transcripts delta (the other six tools verified unchanged); athena untouched. Each new flag installs via the uv stack, so each got a -> local.python.enable assertion — the no-dependency-resolution rule again: a cross-capability requirement has only a failing assertion, no native satisfaction.

Net: python.tools (a seven-tool mechanism-bag) is now four purpose/program-scoped gates, with the uv-install machinery still DRY. The capability surface expresses host intent rather than install plumbing. Contract-test caveat: only dataScience deploys a checkable artifact; the other three install via activation content with no surface, documented inline.

Next Actions (2026-07-20)

  • Run hm switch on athena to validate the 26.05 pin end to end (build-time, not just eval). Drive it over SSH per the athena bring-up notes. If a package fails to build, it will be a stylix/uv2nix-vs-26.05 skew, fixed per-package.
  • Desktop hm switch will now rebuild once (small): the athena pin left atlas byte-identical, but removing claude-code-transcripts changed atlas’s installUvTools script. Expected, one-time.
  • Optional: home.enableNixpkgsReleaseCheck = false on the server profile to silence the deliberate HM-master-on-26.05 mismatch warning.
  • hm switch on the desktop is a no-op (drvPath unchanged), but confirm on next routine switch.

Update 2026-08-04 — uv project registry; lib.uv retired; devTools gate closed

Five commits, merged to main by fast-forward (branch deleted); not yet pushedmain is ahead of origin/main by 5. Opened as a question about moving the Python build trees under pkgs/ with a per-package enable flag; two of those three ideas did not survive. See 2026-08-04 uv Project Registry and the devTools Gate.

Two of the five are follow-ups from review: 2ef6930 fixes the root flake’s pre-commit failure (see Next Actions below — the diagnosis in the first pass was wrong), and 338d009 corrects a claim I had put in the registry header. I had written that obsidian-mcp-proxy “could be built by a module and tested by nothing.” It is tested, by tests/obsidian-mcp-bridge, and always was — that check landed in the same commit that created the project (0812ce3). Its absence from the old literal list was not neglect either: the list predates the project by a month. Nothing had drifted, so the comment was naming a failure that never happened to justify a change that stands on its own.

The move-under-pkgs/ half is rejected. pkgs/ holds seven third-party build recipes (600-2000 bytes each, source pinned by flake = false inputs); the uv projects are first-party workspaces of 45-56 tracked files. And files/scripts/ is one deploy tree — shell.nix:420 string-appends into it for ~/bin, plus lib/scripts.nix, tmux.nix, sqlite-seed.nix, signoz.nix, lib/agent-state.nix — so moving only the Python subdirectories splits it for nothing. The derivations never lived there either way.

The per-package-flag half is rejected on the grounds already recorded here. Dendritic lists boolean enable options as an anti-pattern; the 2026-07-20 entry framed that as the tax auto-discovery charges. The right lever was the flag that already exists.

Commit b55df95 — nix/lib/uv-projects.nix names the five projects once. Previously implied rather than declared: four modules each called lib.uv.mkProject on a relative path, and tests/uv-projects.nix carried a literal list of four names plus a second copy of the pythonSet assembly from lib/uv.nix. The duplicate build was the part that mattered — a build-system override or sourcePreference change had to be made in two files to take effect, so a suite could pass against an environment the shipped package never used. The registry feeds perSystem.packages, so easyOverlay carries each to pkgs.<name> and nix build .#claude-ops works, matching ./pkgs reachability; checks now receives the built packages, not a second name list. It throws in both directions (unregistered uv.lock on disk, registered project gone), both branches tested with a probe directory before wiring in.

lib.uv is retired — the centerpiece of 7b40430 six weeks ago. Once every call site resolved through pkgs.<name>, the lib.extend in mkHome had zero consumers. That refactor was the correct fix for ambient _module.args.uvLib injection; the registry subsumes the problem rather than reversing the judgement.

Commit 15395f6 — claude-ops and gh-review-preview now gate on local.devTools.enable. They deployed on every host importing shell.nix, athena included, so a headless Intel server built duckdb and bashlex for a session-analysis tool with no sessions. This completes the 2026-07-20 story rather than correcting it. That change decoupled the builder from local.python.enable, which was right, and left the tools gated on nothing — and the comment explaining the half it had solved (self-contained venvs need no uv CLI) made the remaining gap read as deliberate. python.enable answers whether a host can run them, never whether it wants them. Contract test written first; failed on all three surfaces before the guard existed.

Resolves two open Next Actions above:

  • Decide: guard uvLib in claude.nix, or narrow the local.python.enable description. Both, effectively. There is no uvLib left to guard, and machine.nix’s description no longer claims python.enable covers the uv2nix-built tools. home-server.nix carried the same false claim and is corrected too.
  • Extend contract coverage to local.devTools.*, which has no false-state test yet. Done, covering both packages and the completion activation.

Also deleted files/scripts/bash-command-validator: 690 files / 33 MB of .venv and caches with zero source files, orphaned when the project’s source was removed.

atlas drvPath byte-identical at agxwsrv407cyl33fbjbxvglcvjrh4v7i across every step including the consumer conversion — which is what proved the registry-built packages are the same derivations, rather than merely suggesting it. nix flake check green; athena evaluates and now carries neither tool.

Correction to the dendritic cost estimate recorded above. The 2026-07-20 framing (and my own reasoning this session) assumed auto-discovery collapses axis 1. It does not: import-tree auto-imports the aspect definitions, while hosts still choose which aspects to import. Dendritic would preserve the curated import list, not eliminate it. That removes the mechanism this note has twice cited as dendritic’s main advantage here — collapsing axis 1 so no composition boundary can mask a dead flag. Worth re-reading the 2026-07-19 and 2026-07-20 dendritic passages with that in mind.

Decision: not adopting dendritic now. ~50-module restructure against a config that works and is documented by ten-plus ADRs. Revisit when a NixOS host starts sharing features with the Darwin hosts; hosts/obsidian-mcp is that host today and shares nothing.

Refinement (2026-09-04): auto-discovery forces an inert default, not a boolean. The 2026-07-20 claim that dendritic’s no-enable stance is “incompatible with auto-discovery by construction” is too strong. A module whose config is mkIf (declaration != []) is inert when auto-imported and active once a profile or host declares something, the way home.file and launchd.agents already behave. programs.homebrew (commit ae0d70b) is the first module built this way: no enable, gated on a non-empty taps ++ brews ++ casks, with an empty-declaration fixture in tests/homebrew.nix proving inertness. A boolean is still right where “off” must survive a declaration (litellm on athena) or where a capability flag needs a handle. See 2026-09-04 The Brewfile That Passed Because It Was Empty.

Next Actions (2026-08-04)

  • Merge refactor/uv-project-registry into main. Done, fast-forward, branch deleted. Re-verified on main after the merge: root and nix/ nix flake check both green, just check green, athena evaluates. atlas drvPath still agxwsrv407cyl33fbjbxvglcvjrh4v7i, unchanged from before any of this work — so the desktop hm switch is a confirmed no-op.
  • Push main (3 commits ahead of origin/main; the five branch commits reached the remote already).
  • packages.<system>.<uv project> is now defined for all four systems including x86_64-darwin. Evaluation only and nix flake check is green, but confirm this does not reintroduce what excluding athena from checks was avoiding.
  • Pre-existing and unrelated: the root flake’s pre-commit-run fails on nix/tests/ipython-config/test_ipython_config.py (ruff skew against the prek that just check runs). Fixed (2ef6930), and it was neither a skew nor unrelated to this repo’s shape. Ruff’s isort infers first-party by resolving against src, which defaults to the project root — and this repo’s root is ~/.config, which carries a gitignored ipython/ directory (the IPython user config home-manager deploys). APFS is case-insensitive, so IPython resolves first-party in a working tree; a flake sees only git-tracked files, so it resolves third-party in the sandbox. Same ruff, same config, opposite answers, and the file could not satisfy both. Fixed by declaring known-third-party = ["IPython"] in a ruff.toml scoped to that test directory. This is a structural hazard of rooting the repo at ~/.config: every gitignored directory there (nvim/, zsh/, nix/, …) is a candidate first-party module name for any tool that infers from the filesystem.
  • athena hm switch still outstanding from 2026-07-20; now also validates that it drops both tools.
  • Consider whether other loose .py files carry the same latent isort ambiguity. Only IPython errors today; the collision class is structural.

Update 2026-08-04 (later) — the last PEP 723 scripts packaged, and their lint debt paid

Three more commits on main, continuing the same session. Same journal entry: 2026-08-04 uv Project Registry and the devTools Gate.

Commit 052b576 — deleted claude-with-timeout. An abandoned prototype whose body says # this is conceptual - actual detection TBD, with a FIFO written by a monitor subshell and read by nobody. Read before removing.

Commit d4256a1 — arxiv-mcp-server, acl-mcp-server and check-upstream-issues became registered uv projects. They were PEP 723 scripts run via uv run --script, which re-resolves the dependency tree from PyPI on every invocation: for the two MCP servers that means the server needs network to boot, its version set drifts silently, and nothing can be built or exercised in a sandbox. mcp.nix now launches them from the store. Generated from the house copier template (gh:achhina/python-project-template v1.8.0) rather than hand-rolled — the convention was discoverable from the .copier-answers.yml every existing project already carries. Sources moved with git mv; all three picked up pytest checks automatically because the template generates tests/ and the registry keys its check derivation off pathExists src/tests.

Two corrections from that work worth keeping: the acl-mcp-server dependency conflict I reported was self-inflicted (requires-python = ">=3.11"uv lock resolves universally across every interpreter in the declared range, so one unsatisfiable Python fails the whole lock), and adding lxml-stubs to fix one ty error produced 53 more, because the stubs type xpath() as a wide union needing casts at ~50 sites.

Commit a4a6684 — the packaging commit’s per-file ignores were debt, and are now paid. Holding those scripts to the template rule set for the first time surfaced 16 violations, and the packaging commit suppressed all of them in one per-file-ignores block under one comment. That block mixed two different things: RUF029/PLW0603/PLC0415 describe how an MCP server is shaped (coroutine tool handlers, a module-global rate-limit clock, a deferred heavyweight import), while A002, B904, E501, N818, PLR0911, PLR0912, PLR0917 and PLW0717 were unpaid work. In TOML the two are indistinguishable.

Fixed rather than suppressed. arxiv: Error-suffixed the two private S2 exceptions, chained the exhausted-retry raise with from err, renamed daily_papersformat to feed_format, and made harvest_metadata / s2_search take their optional filters keyword-only (free, since MCP and click both already pass by name). check-upstream-issues: format_time_ago went from a 13-statement try to one narrow try plus a shared pluralizer, and main’s per-issue loop moved out to collect_statuses.

Both refactored functions got a characterization suite first; the 16 format_time_ago cases passed against the old implementation before the rewrite. Then cleared the ignore table entirely and re-ran to confirm every surviving entry still fires — all six do, and each now carries its own reason. Worth remembering: ruff reports an unused noqa, but never an unused per-file-ignores entry, so nothing tells you when a suppression goes stale.

All gates green (just check, root and nix/ nix flake check, each project’s own just check); the three store binaries were run directly and the three changed CLI commands exercised end to end. athena still carries none of the five devTools packages.

Commit bb191d8 — the last suppression, the lxml one, is gone too. It was an inline ty: ignore on the lxml import, justified by a comment claiming lxml-stubs would need a cast at ~50 call sites. Wrong in shape: all 48 diagnostics land in one contiguous parsing block routing through three xpath result shapes, so three narrowing wrappers (_xs scalar, _xstrs string list, _xels element list) retire every one. lxml-stubs sits in the type-check group, verified absent from the built store path’s closure. The parsing core had no coverage and this rewrote it, so it now has a fixture-XML suite over all four parsers.

Commits dda09a3 and 25f547e — the MCP servers’ three “structural” ignores, checked rather than restated. Only PLC0415 survived.

  • RUF029 was waived on the claim that the handlers “cannot become sync without changing how they are registered.” fastmcp registers sync tools, and FunctionTool defaults run_in_thread=True for them. The waiver was hiding a defect: declared async with no await, eight tools ran blocking work on the event loop (acl’s six all call _load_anthology()). Converted; _run gained an isawaitable branch, the only thing that coupled them to async.
  • PLW0603 is real shared state, which argues for naming it rather than rebinding a module global from a coroutine. Both servers use a _RateLimitState dataclass; the Anthology memo became functools.cache.
  • The audit also found _load_anthology() called at module scope, so importing acl’s cli loaded the corpus and every subcommand paid — --help took 8.7s, now 1.3s with the load moved into serve.
  • 25f547e retires acl’s file-level ruff: file-ignore[blind-except, try-except-pass], which was written for _paper_to_model but also covered a blind except 280 lines away. All best-effort reads funnel through one _best_effort guard, with tests for the degradation contract.

Suppression inventory across the three projects, after all of this: one inline suppression, on _best_effort; per-file ignores are the template’s test and docs/conf.py defaults, plus PLC0415 per MCP server, three subprocess rules in check-upstream-issues (S404/S603/S607), and PLC2701 on arxiv’s and acl’s tests so they can import module-private helpers. Every one verified load-bearing by clearing the table and re-running; each carries its own reason.

Commit b6d63e5 — the author/papers failure, fixed in both the data and the check. ~/.acl-anthology-data was a February checkout 5108 commits behind, from before upstream replaced data/yaml with data/json. The person, venue and SIG indices each read a file under json/ and are built lazily, so the bad load succeeded and only three of six subcommands raised, at first use.

Correcting an earlier note in this entry: from_repo does not build indices the disk branch skips — it is clone_or_pull_from_repo plus the same Anthology(datadir=...). The difference is the pull. That made the disk branch a trap: gated on data/xml alone, it accepted the half-populated checkout and then never updated it, so one upstream layout change froze it broken indefinitely. _data_is_complete now names every directory the reader opens and falls through to from_repo otherwise, which repairs. The local checkout is fast-forwarded (ea3e49a → 3bbf46af) and all six subcommands are verified against the store binary.

Update 2026-08-04 (later still) — uv projects moved to nix/projects/

Commit d44c48e. Reopens and reverses the “leave them in files/scripts/” half of this morning’s decision, on the evidence rather than the argument.

files/scripts/ had become two unrelated things under one name: ~20 loose scripts plus tmux/, read by path from shell.nix, tmux.nix, glance.nix, sqlite-seed.nix, signoz.nix, packages.nix, claude.nix and agent-state.nix; and eight uv projects, read by path from exactly two lines (the registry root and the obsidian-mcp-bridge test). The rejection rested on “it is one deploy tree, splitting it gains nothing”, which is true of the scripts and was never true of the projects — they reach modules as pkgs.<name> through the overlay. They also sat under home-manager/files/, which means “file sources for home.file”, something they never participate in.

Now nix/projects/, a sibling of pkgs/ and lib/. The original pkgs/ proposal stays rejected for the reason recorded this morning: that directory is third-party build recipes over flake = false inputs. nix/AGENTS.md now carries a table stating which of the three homes new code goes in, so the next reader does not have to reconstruct this.

Five of the eight package derivations are byte-identical across the move — the proof it is inert. The three that changed are files edited alongside it: _run’s type annotation in both MCP servers, and a gitleaks inline allow in claude-skill-sync. athena’s activation drvPath is unchanged; atlas’s moves by exactly those three.

Latent problems the move surfaced, worth remembering as a class:

  • shell.nix still claimed the ~/bin python scripts declare deps in PEP-723 headers — false since the morning’s packaging.
  • The same pinned ty 0.0.65 typed _run differently in-project vs in the prek hook, because they resolve against different environments. Fixed by making the type not depend on that.
  • gitleaks flagged PBKDF2 test vectors that had never been inside a staged diff, since prek run only sees staged files. A green gate says less about the tree than it appears to.

Update 2026-08-04 (last) — files/scripts/ is now shell-only

Commits 254126b and 0c95ea2. Closes the packaging arc: every first-party Python tool in this repo is a locked uv workspace under nix/projects/, and nothing in files/scripts/ is deployed as a raw file source any more.

The question was whether to move everything and delete scripts/. No — the inventory settles it. Of 21 tracked entries, 19 are read by path into derivations or spliced into Nix strings; a bash file handed to writeShellApplication has no workspace to lock and cannot get a registry entry, since the registry’s on-disk test for “is this a project” is a committed uv.lock. Moving them would restore the two-things-one-name problem d44c48e just fixed.

The remaining two — the only actual home.file sources in the directory — were parse-history and parse-claude-tools, uv run --script files with PEP 723 headers. Packaged. Their dependencies were unpinned and unbuilt, so parse-claude-tools was one release of claude-code-log away from breaking with nothing in nix flake check to catch it — and it never imported claude-code-log at all. Both now gate on local.devTools.enable, which removes them from athena; parse-claude-tools reads the same ~/.claude/projects transcripts as the already-gated claude-ops, so this extends a decision rather than making one.

Two structural findings, both about gates that were not looking:

  • .gitignore had an unanchored .pre-commit-config.yaml, meant for the root config git-hooks.nix generates. It also matched every per-project config, and prek discovers a nested project only when its config is tracked — so prek list reported six projects where the directory holds eight, and just check returned 0 across two projects it never opened. The six that predate this were force-added past the pattern one at a time, which is why the pattern itself stayed wrong. Anchored. This is the repo’s second unanchored-pattern incident.
  • The template diverges from all nine projects in two places. At the same _commit: v1.8.0, every existing project had hand-fixed .envrc (a shellcheck directive that does not actually silence SC1091 without -x) and flake.nix (an unused self in the outputs lambda). Both fixes were reapplied a ninth and tenth time here. Worth pushing upstream to gh:achhina/python-project-template, since copier update will keep reintroducing them.

nix/AGENTS.md corrected: it described files/scripts/ as deploying via home.file, the house rule for 2 of 21 entries.

Then renamed the directory itself (b3ad2cd): home-manager/files/scripts/nix/scripts/, a sibling of pkgs/ and projects/. With the last two file sources packaged, home-manager/files/ described none of its contents. The eight entries that remain under the old path — tridactylrc, dashboards, the atuin seed history — are what the name is actually true of.

Verified inert by diffing closures rather than assuming: athena byte-identical, and atlas’s single difference is the SigNoz alert annotation naming patch-otel-config.py, a doc pointer inside an alert body. That will churn the alert on the next tofu apply, which is expected. All nine ~/bin derivations rebuild and resolve.

ADR references under docs/decisions/ deliberately left stale — a dated record is not a live pointer, and 0004 had already carried a stale scripts path since d44c48e.

Update 2026-09-04 — athena drops two uv projects (the Intel wheel cliff, second instance)

Commit 81e1e15. acl-mcp-server and arxiv-mcp-server are now excluded from perSystem.packages on x86_64-darwin. They reach pymupdf4llm, then pymupdf-layout, then onnxruntime, whose locked 1.28.0 publishes neither an Intel macOS wheel nor an sdist. Unlike the cryptography case from July there is no lock to fork and no sdist to source-build, so dropping the two projects on that one system is the entire fix.

Safe because of two properties this project already established: home-server.nix curates its imports by hand and does not import coding-agents/common/mcp.nix, and checks maps over hostHomeConfigurations, which deliberately omits x86_64-darwin.

The trap worth recording is the gate. The first attempt keyed on pkgs.stdenv.hostPlatform.isx86_64, which also matches x86_64-linux. Those are the corp workstations, they run the full desktop profile, and mcp.nix puts both servers in home.packages unconditionally, so that gate would have broken a deployed host with a missing-attribute error while leaving the Darwin problem untouched. A platform exclusion in this flake belongs on the full system double, not on an arch or OS predicate: isx86_64, isDarwin and isLinux all read as safe narrowings and each silently covers a host you were not thinking about. Verified by evaluating homeConfigurations."x86_64-linux".config.home.packages rather than by reading.

Two related fixes in the update pipeline the same day (1b45d0d): nix flake check ./nix resolved against the caller’s cwd rather than the config being updated, and the flake-input count in the commit body doubled every input because a unified diff carries both the removed and the added "lastModified" line.

Verification note: the pytest suite under nix/tests/update-changelogs/ was green against a flake.nix that would not parse, because those tests assert on the text of the update script. The instrument that would have caught it is nix flake check, and the change under test was to the script that runs it.

Next Actions (2026-09-04)

  • hm switch on athena to confirm the exclusion actually unblocks its build; 2026-09-04’s verification was evaluation-only from atlas.
  • Consider a check that evaluates every system’s package set, so a parse or attribute error in flake.nix cannot ride in behind a green pytest run.
  • Restore both projects if onnxruntime ships an Intel macOS artifact again.