Skip to content

Latest commit

 

History

History
573 lines (422 loc) · 22.9 KB

File metadata and controls

573 lines (422 loc) · 22.9 KB

Agent Guide for nixcfg

CLAUDE.md is a symlink to this file. Edit AGENTS.md, not CLAUDE.md, and keep the symlink intact.

Start Here Every Time

Before changing code, do the smallest possible orientation pass:

  1. Check the worktree:
    • git status --short
    • git diff --stat
    • git diff --cached if anything is staged
  2. Identify the subsystem from the symptom.
  3. If the task is part of ongoing work, inspect continuity sources:
    • .dex/tasks.jsonl
    • git stash list
    • recent Pi sessions under the repo-specific directory in ~/.pi/agent/sessions/
    • recent Claude sessions under the repo-specific directory in ~/.claude/projects/
    • recent OpenCode sessions in ~/.local/share/opencode/opencode.db filtered to this worktree
    • recent Codex sessions under ~/.codex/sessions/ that mention this worktree
  4. Prefer a narrow reproduction before a full build or full-closure apply.
  5. Before handoff, review your own diff for regressions, generated-file drift, CI/update fallout, and platform-specific breakage.

Local history for this repo heavily favors root-cause analysis, workflow failure investigation, and explicit self-review passes. Match that style.

Non-Negotiable Guardrails

These are hard rules, not suggestions.

  • Do not use --no-verify with git commit, git push, or related git flows in this repo.
  • If hooks fail, fix the underlying issue or stop and ask the user. Do not bypass the hook as a shortcut.
  • If partial staging or commit-splitting makes hooks awkward, use a safer flow instead: temporary patches, an isolated worktree, or a clean commit split that still allows hooks to run normally.
  • Before push, do not rely on spot checks when repo hooks or CI define stricter gates. Run prek run -a or the exact relevant CI-parity commands on the final tree.
  • Do not push changes while any known required local quality gate is failing.

What This Repository Actually Is

This is not just a personal dotfiles repo. It is a mixed Nix + Python codebase with several public and semi-public surfaces:

  • a personal nix-darwin + Home Manager flake
  • reusable exported darwin, home, and nixos module sets
  • a Python nixcfg CLI in nixcfg.py with substantial logic under lib/
  • update/CI tooling under lib/update/ and lib/recover/
  • a schema/codegen toolchain under schema_codegen.yaml, schemas/, and lib/schema_codegen/
  • custom packages and overlays under packages/ and overlays/

Important entrypoints:

  • flake.nix
  • default.nix
  • lib.nix
  • lib/exports.nix
  • darwin/argus.nix
  • darwin/rocinante.nix
  • home/george/default.nix
  • nixcfg.py

The host entrypoints are intentionally thin. Shared behavior usually belongs in modules/, lib/lib.nix, default.nix, or lib/exports.nix, not in a host file, unless the change is truly machine-specific.

Where To Look First

Host / user configuration

  • darwin/ — host entrypoints
  • home/ — user config, scripts, app configs
  • modules/darwin/, modules/home/, modules/nixos/ — reusable module logic

Packages / overlays / build failures

  • packages/
  • overlays/
  • packages/registry.nix
  • packages/default.nix
  • overlays/default.nix
  • packages/*/sources.json
  • packages/*/updater.py
  • packages/*/Cargo.nix
  • packages/*/crate-hashes.json
  • packages/*/uv.lock
  • overlays/*/sources.json

Update / CI / workflow failures

  • .github/workflows/ci.yml
  • .github/workflows/update.yml
  • lib/update/
  • lib/recover/
  • lib/update/ci/
  • nix run .#nixcfg -- ci --help
  • nix run .#nixcfg -- update --help

Public API / exported framework behavior

  • default.nix
  • lib.nix
  • lib/lib.nix
  • lib/exports.nix
  • tests/nix/default-api/default-api.nix
  • lib/tests/test_update_nix_ast.py

OpenCode / MCP / profile wiring

  • modules/home/opencode.nix
  • modules/home/profiles.nix
  • darwin/argus.nix
  • darwin/rocinante.nix

Zen / Twilight browser work

  • modules/home/zen.nix
  • home/george/bin/zentool
  • home/george/zen/

Dominant Workstreams From Repo History

Recent Pi and OpenCode history for this worktree is dominated by five themes:

  1. Periodic Flake Update and CI failures
  2. Argus system closure regressions
  3. Package / overlay / crate2nix / hash drift breakage
  4. OpenCode / MCP / profile configuration
  5. Audit, simplification, and self-review before handoff

That history suggests the right default behavior:

  • start from the failing artifact, job, derivation, or package
  • assume the visible failure may be downstream of a different root cause
  • review staged or final diffs explicitly before finishing
  • avoid broad rewrites when a targeted fix is available

A recurring pattern: failures often surface through argus, but the real bug lives in packages/, overlays/, or lib/update/.

Repo Patterns You Must Preserve

1. flake.nix pins are often intentional operational workarounds

flake.nix contains comments and pins for active upstream breakage or behavior differences. Examples include temporary forks, pinned revisions, and disabled integrations. Do not casually “clean up” those pins without checking why they were introduced.

Homebrew tap inputs are part of this rule. For example, homebrew-cask may be pinned because upstream cask syntax can get ahead of the Homebrew/Ruby runtime available through this flake, such as bare depends_on :macos requiring newer Homebrew behavior. If Homebrew activation fails, inspect flake.nix, modules/darwin/homebrew.nix, and the relevant tap pin before unpinning or updating the tap.

2. Package discovery is structured, not ad hoc

  • packages/default.nix and packages/registry.nix are part of the packaging architecture.
  • Packages are auto-discovered.
  • System constraints live in packages/registry.nix.
  • Companion crate2nix-src.nix entries are discovered and exported too.

If you add or rename a package, check discovery and system constraints, not just the derivation itself.

3. Overlays are fragment-based

overlays/default.nix auto-imports overlay fragments and combines them with helper overlays. Use overlays/ when you need final / prev semantics or are overriding nixpkgs behavior. Use packages/ for standalone callPackage derivations.

4. Source-backed packages use shared selfSource plumbing

The repo now centralizes sibling sources.json injection through:

  • lib/package-self-source.nix
  • default.nix
  • packages/default.nix

If a package expects selfSource, make sure the sibling sources.json pattern still works through the shared helper instead of inventing a one-off path.

5. Darwin app packaging has a shared pattern

For macOS GUI apps, prefer the shared pattern built around:

  • overlays/_lib/helpers/darwin-apps.nix
  • lib/mac-apps.nix
  • passthru.macApp
  • nixcfg.macApps.systemApplications

Do not add bespoke activation hacks for /Applications unless the shared pattern genuinely cannot express the need.

6. default.nix / lib.nix / lib/exports.nix are API surfaces

The repo exports constructors and module sets for downstream use. If you touch these files, treat them as public API and validate accordingly. Preserve constructor names and module export intent unless the task explicitly changes API shape.

7. Prefer AST-backed Nix generation from Python

When Python needs to construct or compose Nix code, prefer nix-manipulator AST builders over raw Nix string templates. For Nix-native behavioral checks, prefer dedicated .nix test files or flake checks instead of embedding multiline Nix programs in Python. Only use inline Nix strings in Python when they are clearly simpler and better for the task at hand.

Python Test Philosophy

Apply this guidance to all maintained Python surfaces in this repo, including nixcfg.py, lib/**/*.py, packages/**/*.py, overlays/**/*.py, modules/**/*.py, and extensionless Python entrypoints.

Treat Python tests as the executable spec for maintained Python behavior. Aim for complete branch coverage of maintained Python logic, including success paths, failure paths, and edge cases. Treat uncovered branches as either missing tests or a design smell that should be simplified or refactored.

Use this testing order by default:

  1. pure Python unit tests
  2. Nix AST assertions with nix-manipulator
  3. mocked Python-side subprocess or wrapper tests
  4. real Nix evaluation only when the semantic property cannot be established above

When Python produces, rewrites, or composes Nix, test the result structurally. Prefer nix-manipulator builders and AST assertions through the shared test helpers. Prefer AST equality over string equality unless exact rendered text is itself the contract.

Do not assert that source files contain or omit raw string fragments. If a test needs to protect source behavior, parse the source and assert on the semantic structure: Nix ASTs for Nix, Python AST or CST for Python, JSON/TOML/YAML parsers for data files, and shell/JS syntax or runtime harnesses for embedded scripts. Keep substring assertions for presentation output only when the text itself is the user-facing contract.

Do not use Nix evaluation merely to prove that Python emitted the right Nix. If production code currently returns Nix text, parse it in the test and compare ASTs. If that is awkward, prefer refactoring the production code toward explicit Nix AST builders.

Treat real Nix evaluation as an exception. Use it only when the semantic property cannot be established at the AST level. Keep those evals extremely lightweight: import only the smallest unit under test, use tiny --expr harnesses, return JSON or raw scalars, and never trigger builds, flake checks, host evaluations, closures, or broad module graphs from pytest.

When Python code shells out to Nix or subprocess helpers, test Python behavior at the Python boundary first. Mock run_nix, run_nix_json, subprocess.run, or equivalent wrappers to cover control flow, argument construction, parsing, timeouts, and failures. Reserve real Nix invocation for narrow semantic checks that cannot be replaced with AST or mocked-boundary tests.

Any new eval-based test must include a short docstring or comment explaining why AST-only or pure- Python testing was insufficient.

If existing code or tests do not fit this philosophy, refactor them as they are encountered. Prefer separating pure Python decision logic, Nix AST construction/rendering, and thin subprocess/evaluation adapters. Do not preserve heavyweight, stringly, or broad-eval tests just for consistency with older patterns. Move them toward pure Python and AST-based assertions when touched.

Investigation Rules

Prefer targeted root-cause analysis

Start from one of these before doing anything expensive:

  • the failing workflow job
  • the failing derivation log
  • the specific package or overlay
  • the generated artifact that drifted
  • the exact staged diff that introduced the change

Do not accept “build from source” as a substitute when caches should exist

If the user expects cached or prebuilt outputs, investigate why a cached path, version, or hash diverged. Do not assume a from-source rebuild is an acceptable outcome.

Preserve platform guards

This flake exports:

  • aarch64-darwin
  • aarch64-linux
  • x86_64-linux

Do not remove or weaken platform guards casually. Many recurring failures in this repo are caused by platform-specific drift.

Treat hash and generated artifact drift as first-class issues

Common failure classes here:

  • sources.json drift
  • uv.lock drift
  • Cargo.nix / crate-hashes.json drift
  • packages/superset/bun.nix drift
  • npmDepsHash / denoDepsHash / vendorHash mismatches
  • workflow artifact naming/path mismatches

The workflow YAML for update.yml, ci.yml, and update-certify.yml is generated from typed Python models. Never hand-edit those files; change the defs and regenerate. Relevant places:

  • lib/update/ci/workflow_defs.py (source of truth)
  • lib/update/ci/workflow_model.py (model, renderer, structural invariants)
  • nixcfg ci workflow generate / nixcfg ci workflow verify-generated
  • lib/tests/test_ci_workflow_defs.py

When simplifying, remove accidental complexity without breaking discovery or exports

Recent history includes several simplification passes. Good simplifications here usually:

  • deduplicate helper plumbing
  • reduce repeated let-bindings in thin derivations
  • move common package/update logic into shared helpers

Bad simplifications usually:

  • hide platform distinctions
  • bypass package/overlay discovery
  • break exported constructors or module names
  • inline generated values that should stay machine-maintained

Task-Specific Guidance

Fixing argus or rocinante build failures

Start with:

  • nix build .#checks.aarch64-darwin.darwin-argus
  • nix build .#checks.aarch64-darwin.darwin-rocinante

If the failure mentions a package, switch quickly into that package or overlay instead of staring only at the host module.

Adding or updating a package

Check these first:

  • packages/registry.nix
  • similar existing packages in packages/ or overlays/
  • packages/default.nix
  • lib/package-self-source.nix for sibling sources.json / selfSource plumbing

For source-backed packages, make sure the full pattern is coherent:

  • derivation file
  • sources.json if needed
  • updater.py if needed
  • system constraint in packages/registry.nix if not universal
  • validation build for the real target platform

Updating CI or update workflow logic

Understand which phase you are touching. The update workflow is phase-structured: lock update, version resolution, per-platform hash computation, merge, crate2nix refresh, then downstream validation/build steps. Make changes phase-consciously.

Working on OpenCode / MCP / profile setup

Use modules/home/opencode.nix and modules/home/profiles.nix as the source of truth. Do not scatter MCP or profile behavior across unrelated host files unless the behavior is truly host-specific.

Working on Zen / Twilight

Use the dedicated module and repo-managed Zen tooling:

  • modules/home/zen.nix
  • home/george/bin/zentool
  • home/george/zen/folders.yaml

Remember that Zen folder reconciliation is stateful and interacts with a live browser profile.

Catppuccin chrome theme wiring — do not touch unless explicitly asked

The Zen chrome theme (userChrome.css, userContent.css, zen-logo-<variant>.svg) is not maintained in this repo. It is sourced from the catppuccin-zen-browser flake input, which pins the gkze/zen-browser fork on branch fix/frappe-zen-twilight-acrylic-gap until that fix lands upstream in catppuccin/zen-browser. See the committed architecture at home/george/configuration.nix — the zen.chromeSource is a pkgs.runCommand derivation that symlinks the three files from themes/${Variant}/${Accent}/ in the flake input, driven by config.theme.variant and config.theme.accentColor.

Hard rules for this setup:

  • Do not re-add home/george/zen/chrome/ with hand-copied userChrome.css / userContent.css / zen-logo-*.svg. Those files belong upstream in the fork. If a theme tweak is needed, make it on the gkze/zen-browser fork branch and bump the flake input.
  • Do not add a home.activation.directZenRepoSymlinks (or similar) script that live-links ~/.config/zen/chrome/* to working-tree paths. The declarative chromeSource path already materializes the correct store path; zentool then syncs it into the live profile.
  • Do not set nixcfg.zen.chromeSource = null in home/george/configuration.nix. null disables the Nix-store materialization and leaves the profile unthemed.
  • home/george/zen/user.js is genuinely local (not a copy of upstream) and contains the ui.systemUsesDarkTheme = 1 workaround for zen-browser/desktop#9542. Without that pref, every @media (prefers-color-scheme: dark) rule in the Catppuccin userChrome.css evaluates false and the whole theme silently fails to apply. Do not remove that pref.

If you think the chrome theme is broken, the first diagnostic is almost always one of:

  1. Uncommitted regression in flake.nix / flake.lock / home/george/configuration.nix that reverted the fork-sourcing. Run git diff HEAD -- flake.nix flake.lock home/george/configuration.nix home/george/zen/ first.
  2. Missing ui.systemUsesDarkTheme pref in home/george/zen/user.js.
  3. Drift between the fork branch and current Zen internals — fix in the fork, not here.

Known Twilight quit-dialog lessons from the 2026-04 upgrade:

  • Do not treat a bad quit dialog as proof that userChrome.css is not loading. First prove chrome CSS loading with a temporary, unmistakable wrapper/background marker outside @media (prefers-color-scheme: dark), restart Twilight, and remove the marker after the check.
  • The quit warning is chrome://global/content/commonDialog.xhtml. Its buttons are created inside the <dialog id="commonDialog"> shadow root, and every button is only exposed to outer userChrome.css as part="dialog-button". Pure dialog::part(dialog-button) rules can theme the dialog shell and secondary buttons, but cannot reliably target only the accept/Quit button or its internal .button-box / .button-text.
  • For accept-button fixes, use the repo-managed AutoConfig path in home/george/zen/autoconfig/. Keep it as a minimal bridge: run in the common-dialog document, strip the native default attribute from button[dlgtype="accept"] / label^="Quit ", preserve the existing dialog-button part, and add the catppuccin-primary-button part token so the Catppuccin userChrome.css can own the actual colors and hover/active states. In AutoConfig, prefer XPCOM services from Components.classes[...]; importing Services.sys.mjs can fail in that context.
  • The safe package path is loose AutoConfig files plus ad-hoc re-signing: install autoconfig.js under both Contents/Resources/defaults/pref/ and Contents/Resources/browser/defaults/preferences/, install twilight.cfg under both Contents/Resources/ and Contents/Resources/browser/, then sign the final app bundle. Do not patch browser/omni.ja with /usr/bin/zip; that made Twilight launch and immediately quit.
  • Do not leave exploratory diagnostics in prefs.js or the app bundle. If you add temporary prefs, visible CSS markers, copied files, or backup bundles while debugging, remove them before handoff.

Validation Ladder

Use the narrowest relevant checks first. Do not jump straight to the most expensive command unless the task needs it.

Formatting and local hooks

  • nix fmt
  • prek run -a

Python quality

  • uv run ruff check --config pyproject.toml .
  • uv run ty check .
  • uv run pytest
  • uv run coverage run -m pytest && uv run coverage report

Quality bar reminders:

  • Python is 3.14+
  • Ruff is configured aggressively
  • ty warnings fail
  • coverage floor is 100%, with explicit reason-annotated exclusions

Coverage exclusion policy: the floor stays at 100%, but a line or branch may be excluded with # pragma: no cover -- <reason> / # pragma: no branch -- <reason> (the reason is mandatory; bare pragmas do not exclude anything). An exclusion is justified only when a test would merely restate the implementation — defensive re-raises, __main__ guards, abstract method bodies, platform branches CI cannot execute. If a branch can fail meaningfully, write a behavioral test instead; if it genuinely cannot execute, consider deleting the branch before excluding it. Whole-module exemptions are not allowed except via an explicit, commented entry in pyproject.toml. Never satisfy the floor by writing tests that mock every collaborator just to walk a line — that is the design smell the policy exists to prevent.

Nix / flake / host outputs

  • nix flake check
  • nix build .#checks.aarch64-darwin.darwin-argus
  • nix build .#checks.aarch64-darwin.darwin-rocinante
  • nix build .#homeConfigurations.george.activationPackage
  • nix build .#pkgs.<system>.<name> for package-focused work

Only use this when the task truly requires an actual local apply:

  • nh darwin switch --no-nom .

Workflow / update / artifact validation

  • nix run .#nixcfg -- ci workflow verify-generated
  • uv run python -m lib.update.ci.crate2nix
  • relevant nix run .#nixcfg -- ci ... subcommands
  • relevant nix run .#nixcfg -- update ... commands

Workflow hygiene tools enforced in CI

  • pinact run --check
  • nix run --inputs-from . nixpkgs#actionlint
  • yamllint -c .yamllint ...

CSS / web-y additions

If the task touches CSS or related frontend-ish repo assets, validate that too:

  • nix build .#checks.x86_64-linux.format-web-oxfmt
  • nix build .#checks.x86_64-linux.lint-web-oxlint

Commit And Review Norms

Recent human-authored commits are overwhelmingly conventional-commit style. Prefer subjects like:

  • fix(ci): ...
  • fix(update): ...
  • fix(packages): ...
  • feat(packages): ...
  • refactor(update): ...
  • chore: ...

Notes:

  • old nix: Update ... and flake.lock: Update ... commits exist, but they are older automation-shaped commits
  • commitlint is enforced
  • if the user explicitly asks you to commit, use git commit -S

Before finishing substantial work, do a review pass against your own diff. OpenCode history for this repo shows very heavy use of explicit self-review subagents; emulate that discipline even when working alone.

Generated, Sensitive, And Local-State Files

Do not hand-edit unless the task is explicitly about regeneration

  • .pre-commit-config.yaml
  • .github/workflows/update.yml, .github/workflows/ci.yml, and .github/workflows/update-certify.yml (regenerate via nixcfg ci workflow generate from lib/update/ci/workflow_defs.py)
  • packages/**/Cargo.nix
  • packages/**/crate-hashes.json
  • packages/**/sources.json
  • packages/**/uv.lock
  • overlays/**/sources.json
  • packages/superset/bun.nix
  • lib/**/_generated.py

Treat carefully

  • secrets.yaml
  • .sops.yaml
  • ~/.pi
  • ~/.claude
  • ~/.local/share/opencode

Use local history for continuity, but do not surface credentials or paste sensitive local data back into the conversation.

Continuity Sources

If the user asks “where were we?”, “what is this worktree trying to do?”, or wants work formalized:

  • inspect git status, git diff, and git diff --cached
  • inspect git stash list
  • inspect .dex/tasks.jsonl
  • inspect recent Pi sessions under the repo-specific directory in ~/.pi/agent/sessions/
  • inspect recent Claude sessions under the repo-specific directory in ~/.claude/projects/
  • inspect recent OpenCode sessions in ~/.local/share/opencode/opencode.db filtered to this worktree
  • inspect recent Codex sessions under ~/.codex/sessions/ that mention this worktree

The local histories are genuinely useful here; this repo has a lot of long-running, iterative debugging and architecture work.

Usually Ignore Unless The Task Is About Them

These are commonly local state or generated artifacts, not source-of-truth code:

  • .direnv/
  • .venv/
  • node_modules/
  • result/
  • .pytest_cache/
  • .ruff_cache/
  • .coverage*