@@ 1,191 @@
+# agent-environments — design
+
+Status: self-authored during an autonomous run (user asked to work autonomously
+and consult Codex). Reviewed by Codex (review-1: blocking points folded in), not
+yet by the user.
+
+## Goal
+
+A Nix flake **library** for declaring reproducible configurations ("environments")
+for coding agents — Claude Code and Codex for now — and launching them under
+separate identities (logins). Other flakes consume it; `examples/` shows how.
+
+## Requirements (from the user)
+
+- Module-based: modules add text to the instruction file (AGENTS.md / CLAUDE.md)
+ and enable skills, e.g. `modules.skills.xyz.enable = true`.
+- Several launchers such as `claude-work-acc`, `claude-pers-acc`. They may share
+ one environment but log in separately. Each launcher gets its own state
+ directory `~/.local/agent-environments/<id>`, passed in with
+ `CLAUDE_CONFIG_DIR` / `CODEX_HOME`.
+- Each environment is its own set of modules.
+- Skills are packaged. A skill can need runtime tools and other skills.
+- Flakes. `claude-code` and `codex` come in as inputs:
+ `github:sadjow/claude-code-nix`, `github:sadjow/codex-cli-nix`.
+- Standalone only (no home-manager). The root flake exposes the library plus one
+ default environment with no skills.
+
+## Architecture
+
+```
+flake.nix inputs: nixpkgs, claude-code, codex
+lib/default.nix mkEnvironment, mkSkill, mkSkillModule
+modules/core.nix instructions, skills, launchers, assertions
+modules/claude.nix claude.{enable,package,settings}
+modules/codex.nix codex.{enable,package,settings}
+modules/launcher.nix wrapper script generation
+checks/ behaviour tests (stub agents, temp HOME)
+examples/<name>/flake.nix consumer flakes
+```
+
+### Evaluation
+
+`lib.mkEnvironment { pkgs, modules ? [] }` runs `lib.evalModules` over the core
+modules plus the user's modules, with `pkgs` passed as a module argument. It
+returns the evaluated `config` along with `package` (a `symlinkJoin` of every
+launcher) and `launchers` (an attrset of per-launcher packages).
+
+The flake's `lib` is system-independent. `mkEnvironment` receives `pkgs`, and the
+flake sets default agent packages for `pkgs.stdenv.hostPlatform.system` from its
+own inputs. Consumers can override those with `claude.package` /
+`codex.package`.
+
+### Core options
+
+- `instructions.<name> = { text : lines; order : int = 1000; }` — fragments
+ sorted by `(order, name)` and joined with a blank line into one document. That
+ document becomes `CLAUDE.md` for Claude and `AGENTS.md` for Codex. Modules add
+ a fragment by defining an entry.
+- `skills.packages : listOf package` — skill derivations made by `mkSkill`. The
+ effective set is the transitive closure over `passthru.skillDeps`, deduplicated
+ by skill name. Two different derivations with the same name → evaluation error.
+- `claude.enable`, `claude.package`, `claude.settings : attrs` (JSON).
+- `codex.enable`, `codex.package`, `codex.settings : attrs` (TOML).
+- `launchers.<name> = { agent : enum ["claude" "codex"]; id : str = <name>;
+ stateDir : str = "~/.local/agent-environments/${id}"; }`. The name is the
+ command name. `stateDir` must be absolute or start with `~/` (assertion); it is
+ shell-quoted as a literal, and only the leading `~/` becomes `$HOME`.
+ A launcher's agent must be enabled (assertion).
+ `id` and launcher names must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` (assertion).
+- Launchers are always declared explicitly. There are no implicit default
+ launchers, because attrset options merge by key and a hidden default could not
+ be dropped cleanly.
+
+### Modules namespace
+
+`modules.*` is where optional features live, e.g. `modules.skills.<name>.enable`.
+The helper `lib.mkSkillModule skill` produces a module declaring
+`options.modules.skills.<skill.skillName>.enable` and, when it is enabled, adding
+the skill to `skills.packages`. Options cannot be generated from `config` (that
+would recurse infinitely), so consumers import one module per skill they want to
+make available. Instruction modules are plain modules that define
+`instructions.<name>`.
+
+### Skill packaging
+
+`lib.mkSkill { pkgs, name, src, runtimeInputs ? [], skills ? [] }`:
+
+- `name` must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` (evaluation-time check).
+- Output: `$out/` is the skill directory (it must contain `SKILL.md`; checked at
+ build time). The same directory works for both agents, since Claude Code and
+ Codex share the `SKILL.md` format.
+- `passthru = { skillName = name; runtimeInputs; skillDeps = skills; }`.
+- Runtime inputs: the launcher puts the union of all effective skills'
+ `runtimeInputs` on `PATH` (prepended). Skills run inside the agent's shell, so
+ this is the smallest mechanism that works for both agents.
+
+### Launcher (runtime)
+
+The wrapper is a `writeShellApplication` per launcher. Everything it manages is
+listed in a manifest, `$state/.agent-environments-managed` (one relative path per
+line). On every start it:
+
+1. `mkdir -p "$state/skills"`, then takes `flock` on
+ `$state/.agent-environments.lock` for the setup phase only; the lock fd is
+ closed before `exec`.
+2. Computes the wanted set of managed links: the instruction file
+ (`CLAUDE.md` for claude, `AGENTS.md` for codex, when there are instructions),
+ `skills/<name>` for each effective skill, and the codex profile TOML (when
+ there are settings).
+3. Removes every path in the old manifest that is not wanted any more, but only
+ when it is still a symlink. Anything else is left in place.
+4. For each wanted path that exists: unless it is listed in the old manifest and
+ is still a symlink, stop with an error naming the path (user data, including
+ user symlinks, is never replaced
+ and there are no automatic backups). Otherwise replace it atomically
+ (`ln -s` to a temp name, then `mv -T`).
+5. Writes the new manifest atomically. Codex's own `skills/.system` and any
+ user-installed skills are never touched, because they are not in the manifest.
+6. Settings, layered so the agents stay free to write their own files:
+ - Claude: `--settings <store settings.json>` when `claude.settings != {}`.
+ `settings.json`, `.claude.json` and `.credentials.json` in the state dir
+ stay owned by Claude.
+ - Codex: `$state/agent-environments.config.toml` (a managed link to the store
+ TOML) plus `--profile agent-environments`, which layers
+ `$CODEX_HOME/<profile>.config.toml` over `config.toml`. `config.toml`
+ (which holds trust entries) and `auth.json` stay owned by Codex. Skipped
+ when `codex.settings == {}`.
+7. `export CLAUDE_CONFIG_DIR="$state"` / `export CODEX_HOME="$state"`, prepend
+ the runtime inputs to `PATH`, then `exec <agent> <managed flags> "$@"`.
+
+Logins are therefore per launcher: `claude-work-acc` and `claude-pers-acc` have
+different ids, so different state dirs, so different credentials, even when they
+come from the same environment. Two launchers that should share a login can set
+the same `id`. They then also share managed content: the last launch wins, so they
+should come from the same environment. This is documented, not enforced.
+
+### Root flake outputs
+
+- `lib` — `mkEnvironment`, `mkSkill`, `mkSkillModule`.
+- `packages.<system>.default` — the default environment: claude + codex enabled,
+ launchers `claude` and `codex` with ids `default-claude` / `default-codex`,
+ no skills and no instructions.
+- `checks.<system>.*` — behaviour tests.
+- `formatter` — `nixfmt`.
+
+### Examples
+
+- `examples/basic` — one environment, one instruction fragment, one local skill
+ with a runtime dependency and a skill dependency.
+- `examples/multi-account` — one module set, launchers `claude-work-acc`,
+ `claude-pers-acc`, `codex-work-acc`, plus a second environment with a different
+ module set.
+
+Each example is a flake with `inputs.agent-environments.url = "path:../..";`.
+
+## Error handling
+
+- Evaluation-time assertions: a launcher's agent is disabled; an unsafe id,
+ launcher or skill name; conflicting skill names. A skill without `SKILL.md`
+ fails at build time.
+- Runtime: never delete non-symlink files and never replace unmanaged ones; fail
+ loudly (`set -euo pipefail`) and say which path conflicts.
+
+## Testing
+
+`nix flake check` runs derivation checks that execute the launchers with stub
+`claude`/`codex` packages (scripts that dump argv, env and state-dir contents),
+using a temporary `HOME`:
+
+- the state dir is created under `$HOME/.local/agent-environments/<id>` and the
+ right env var points at it;
+- two launchers from one environment get different state dirs (separate logins);
+- the instruction file carries the fragments in order;
+- dependency skills are linked transitively; a skill's runtime tool is on `PATH`;
+- a skill that is no longer enabled is unlinked on the next launch, while user
+ skill dirs, user symlinks and `.system` are kept;
+- a pre-existing user `CLAUDE.md` makes the launcher fail and is left untouched;
+- the `--settings` / `--profile` flags appear only when settings are non-empty.
+
+One integration check runs the real Codex package offline to confirm that it
+discovers `$CODEX_HOME/AGENTS.md`, the skills and the profile, if Codex has a
+non-interactive way to show this. If it does not, that is recorded as a coverage
+gap and checked by hand.
+
+Examples are built with `nix build ./examples/<x> --override-input
+agent-environments path:$PWD`.
+
+## Out of scope (YAGNI)
+
+home-manager integration, MCP server modules, plugins, per-project config,
+agents other than Claude Code and Codex.