CLAUDE.md is a symlink to this file. Edit AGENTS.md, not CLAUDE.md, and keep the symlink
intact.
Before changing code, do the smallest possible orientation pass:
- Check the worktree:
git status --shortgit diff --statgit diff --cachedif anything is staged
- Identify the subsystem from the symptom.
- If the task is part of ongoing work, inspect continuity sources:
.dex/tasks.jsonlgit 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.dbfiltered to this worktree - recent Codex sessions under
~/.codex/sessions/that mention this worktree
- Prefer a narrow reproduction before a full build or full-closure apply.
- 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.
These are hard rules, not suggestions.
- Do not use
--no-verifywithgit 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 -aor the exact relevant CI-parity commands on the final tree. - Do not push changes while any known required local quality gate is failing.
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, andnixosmodule sets - a Python
nixcfgCLI innixcfg.pywith substantial logic underlib/ - update/CI tooling under
lib/update/andlib/recover/ - a schema/codegen toolchain under
schema_codegen.yaml,schemas/, andlib/schema_codegen/ - custom packages and overlays under
packages/andoverlays/
Important entrypoints:
flake.nixdefault.nixlib.nixlib/exports.nixdarwin/argus.nixdarwin/rocinante.nixhome/george/default.nixnixcfg.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.
darwin/— host entrypointshome/— user config, scripts, app configsmodules/darwin/,modules/home/,modules/nixos/— reusable module logic
packages/overlays/packages/registry.nixpackages/default.nixoverlays/default.nixpackages/*/sources.jsonpackages/*/updater.pypackages/*/Cargo.nixpackages/*/crate-hashes.jsonpackages/*/uv.lockoverlays/*/sources.json
.github/workflows/ci.yml.github/workflows/update.ymllib/update/lib/recover/lib/update/ci/nix run .#nixcfg -- ci --helpnix run .#nixcfg -- update --help
default.nixlib.nixlib/lib.nixlib/exports.nixtests/nix/default-api/default-api.nixlib/tests/test_update_nix_ast.py
modules/home/opencode.nixmodules/home/profiles.nixdarwin/argus.nixdarwin/rocinante.nix
modules/home/zen.nixhome/george/bin/zentoolhome/george/zen/
Recent Pi and OpenCode history for this worktree is dominated by five themes:
- Periodic Flake Update and CI failures
- Argus system closure regressions
- Package / overlay / crate2nix / hash drift breakage
- OpenCode / MCP / profile configuration
- 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/.
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.
packages/default.nixandpackages/registry.nixare part of the packaging architecture.- Packages are auto-discovered.
- System constraints live in
packages/registry.nix. - Companion
crate2nix-src.nixentries are discovered and exported too.
If you add or rename a package, check discovery and system constraints, not just the derivation itself.
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.
The repo now centralizes sibling sources.json injection through:
lib/package-self-source.nixdefault.nixpackages/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.
For macOS GUI apps, prefer the shared pattern built around:
overlays/_lib/helpers/darwin-apps.nixlib/mac-apps.nixpassthru.macAppnixcfg.macApps.systemApplications
Do not add bespoke activation hacks for /Applications unless the shared pattern genuinely cannot
express the need.
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.
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.
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:
- pure Python unit tests
- Nix AST assertions with
nix-manipulator - mocked Python-side subprocess or wrapper tests
- 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.
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
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.
This flake exports:
aarch64-darwinaarch64-linuxx86_64-linux
Do not remove or weaken platform guards casually. Many recurring failures in this repo are caused by platform-specific drift.
Common failure classes here:
sources.jsondriftuv.lockdriftCargo.nix/crate-hashes.jsondriftpackages/superset/bun.nixdriftnpmDepsHash/denoDepsHash/vendorHashmismatches- 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-generatedlib/tests/test_ci_workflow_defs.py
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
Start with:
nix build .#checks.aarch64-darwin.darwin-argusnix 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.
Check these first:
packages/registry.nix- similar existing packages in
packages/oroverlays/ packages/default.nixlib/package-self-source.nixfor siblingsources.json/selfSourceplumbing
For source-backed packages, make sure the full pattern is coherent:
- derivation file
sources.jsonif neededupdater.pyif needed- system constraint in
packages/registry.nixif not universal - validation build for the real target platform
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.
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.
Use the dedicated module and repo-managed Zen tooling:
modules/home/zen.nixhome/george/bin/zentoolhome/george/zen/folders.yaml
Remember that Zen folder reconciliation is stateful and interacts with a live browser profile.
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-copieduserChrome.css/userContent.css/zen-logo-*.svg. Those files belong upstream in the fork. If a theme tweak is needed, make it on thegkze/zen-browserfork 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 declarativechromeSourcepath already materializes the correct store path;zentoolthen syncs it into the live profile. - Do not set
nixcfg.zen.chromeSource = nullinhome/george/configuration.nix.nulldisables the Nix-store materialization and leaves the profile unthemed. home/george/zen/user.jsis genuinely local (not a copy of upstream) and contains theui.systemUsesDarkTheme = 1workaround 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:
- Uncommitted regression in
flake.nix/flake.lock/home/george/configuration.nixthat reverted the fork-sourcing. Rungit diff HEAD -- flake.nix flake.lock home/george/configuration.nix home/george/zen/first. - Missing
ui.systemUsesDarkThemepref inhome/george/zen/user.js. - 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.cssis 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 outeruserChrome.cssaspart="dialog-button". Puredialog::part(dialog-button)rules can theme the dialog shell and secondary buttons, but cannot reliably target only the accept/Quitbutton 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 nativedefaultattribute frombutton[dlgtype="accept"]/label^="Quit ", preserve the existingdialog-buttonpart, and add thecatppuccin-primary-buttonpart token so the CatppuccinuserChrome.csscan own the actual colors and hover/active states. In AutoConfig, prefer XPCOM services fromComponents.classes[...]; importingServices.sys.mjscan fail in that context. - The safe package path is loose AutoConfig files plus ad-hoc re-signing: install
autoconfig.jsunder bothContents/Resources/defaults/pref/andContents/Resources/browser/defaults/preferences/, installtwilight.cfgunder bothContents/Resources/andContents/Resources/browser/, then sign the final app bundle. Do not patchbrowser/omni.jawith/usr/bin/zip; that made Twilight launch and immediately quit. - Do not leave exploratory diagnostics in
prefs.jsor the app bundle. If you add temporary prefs, visible CSS markers, copied files, or backup bundles while debugging, remove them before handoff.
Use the narrowest relevant checks first. Do not jump straight to the most expensive command unless the task needs it.
nix fmtprek run -a
uv run ruff check --config pyproject.toml .uv run ty check .uv run pytestuv run coverage run -m pytest && uv run coverage report
Quality bar reminders:
- Python is
3.14+ - Ruff is configured aggressively
tywarnings 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 checknix build .#checks.aarch64-darwin.darwin-argusnix build .#checks.aarch64-darwin.darwin-rocinantenix build .#homeConfigurations.george.activationPackagenix 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 .
nix run .#nixcfg -- ci workflow verify-generateduv run python -m lib.update.ci.crate2nix- relevant
nix run .#nixcfg -- ci ...subcommands - relevant
nix run .#nixcfg -- update ...commands
pinact run --checknix run --inputs-from . nixpkgs#actionlintyamllint -c .yamllint ...
If the task touches CSS or related frontend-ish repo assets, validate that too:
nix build .#checks.x86_64-linux.format-web-oxfmtnix build .#checks.x86_64-linux.lint-web-oxlint
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 ...andflake.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.
.pre-commit-config.yaml.github/workflows/update.yml,.github/workflows/ci.yml, and.github/workflows/update-certify.yml(regenerate vianixcfg ci workflow generatefromlib/update/ci/workflow_defs.py)packages/**/Cargo.nixpackages/**/crate-hashes.jsonpackages/**/sources.jsonpackages/**/uv.lockoverlays/**/sources.jsonpackages/superset/bun.nixlib/**/_generated.py
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.
If the user asks “where were we?”, “what is this worktree trying to do?”, or wants work formalized:
- inspect
git status,git diff, andgit 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.dbfiltered 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.
These are commonly local state or generated artifacts, not source-of-truth code:
.direnv/.venv/node_modules/result/.pytest_cache/.ruff_cache/.coverage*